From 8eaaf65456374d1cb3e185b14d9ee46fbb968055 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 3 Apr 2024 11:23:25 -0400 Subject: [PATCH 001/110] Avoid unused extras check in `pip install` for source trees (#2811) ## Summary This was an oversight in the `-r pyproject.toml` refactor. We can't enforce unused extras if we have a source tree. We made the correct changes to `pip compile`, but not `pip install`. This PR just mirrors those changes to `pip install`, and adds a few tests. Closes https://github.com/astral-sh/uv/issues/2801. --- crates/uv/src/commands/pip_install.rs | 47 +++++++++++++------------ crates/uv/src/commands/pip_sync.rs | 1 + crates/uv/tests/pip_compile.rs | 19 +++++++++-- crates/uv/tests/pip_install.rs | 49 +++++++++++++++++++++++++++ 4 files changed, 91 insertions(+), 25 deletions(-) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index e54d64ff0..0dc96ad05 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -385,13 +385,13 @@ async fn read_requirements( extras: &ExtrasSpecification<'_>, client_builder: &BaseClientBuilder<'_>, ) -> Result { - // If the user requests `extras` but does not provide a pyproject toml source - if !matches!(extras, ExtrasSpecification::None) - && !requirements - .iter() - .any(|source| matches!(source, RequirementsSource::PyprojectToml(_))) - { - return Err(anyhow!("Requesting extras requires a pyproject.toml input file.").into()); + // If the user requests `extras` but does not provide a valid source (e.g., a `pyproject.toml`), + // return an error. + if !extras.is_empty() && !requirements.iter().any(RequirementsSource::allows_extras) { + return Err(anyhow!( + "Requesting extras requires a `pyproject.toml`, `setup.cfg`, or `setup.py` file." + ) + .into()); } // Read all requirements from the provided sources. @@ -404,21 +404,24 @@ async fn read_requirements( ) .await?; - // Check that all provided extras are used - if let ExtrasSpecification::Some(extras) = extras { - let mut unused_extras = extras - .iter() - .filter(|extra| !spec.extras.contains(extra)) - .collect::>(); - if !unused_extras.is_empty() { - unused_extras.sort_unstable(); - unused_extras.dedup(); - let s = if unused_extras.len() == 1 { "" } else { "s" }; - return Err(anyhow!( - "Requested extra{s} not found: {}", - unused_extras.iter().join(", ") - ) - .into()); + // If all the metadata could be statically resolved, validate that every extra was used. If we + // need to resolve metadata via PEP 517, we don't know which extras are used until much later. + if spec.source_trees.is_empty() { + if let ExtrasSpecification::Some(extras) = extras { + let mut unused_extras = extras + .iter() + .filter(|extra| !spec.extras.contains(extra)) + .collect::>(); + if !unused_extras.is_empty() { + unused_extras.sort_unstable(); + unused_extras.dedup(); + let s = if unused_extras.len() == 1 { "" } else { "s" }; + return Err(anyhow!( + "Requested extra{s} not found: {}", + unused_extras.iter().join(", ") + ) + .into()); + } } } diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index d9ad30410..acc8ac8df 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -61,6 +61,7 @@ pub(crate) async fn pip_sync( printer: Printer, ) -> Result { let start = std::time::Instant::now(); + let client_builder = BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls) diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index e86c8a971..2debbb05b 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -419,6 +419,10 @@ authors = ["Astral Software Inc. "] [tool.poetry.dependencies] python = "^3.10" anyio = "^3" +pytest = { version = "*", optional = true } + +[tool.poetry.extras] +test = ["pytest"] [build-system] requires = ["poetry-core"] @@ -427,20 +431,29 @@ build-backend = "poetry.core.masonry.api" )?; uv_snapshot!(context.compile() - .arg("pyproject.toml"), @r###" + .arg("pyproject.toml") + .arg("--extra") + .arg("test"), @r###" success: true exit_code: 0 ----- stdout ----- # This file was autogenerated by uv via the following command: - # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z pyproject.toml --extra test anyio==3.7.1 idna==3.6 # via anyio + iniconfig==2.0.0 + # via pytest + packaging==24.0 + # via pytest + pluggy==1.4.0 + # via pytest + pytest==8.1.1 sniffio==1.3.1 # via anyio ----- stderr ----- - Resolved 3 packages in [TIME] + Resolved 7 packages in [TIME] "### ); diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index ee987fb0b..7c2e680f7 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -360,6 +360,55 @@ fn install_requirements_txt() -> Result<()> { Ok(()) } +/// Install a `pyproject.toml` file with a `poetry` section. +#[test] +fn install_pyproject_toml_poetry() -> Result<()> { + let context = TestContext::new("3.12"); + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#"[tool.poetry] +name = "poetry-editable" +version = "0.1.0" +description = "" +authors = ["Astral Software Inc. "] + +[tool.poetry.dependencies] +python = "^3.10" +anyio = "^3" +iniconfig = { version = "*", optional = true } + +[tool.poetry.extras] +test = ["iniconfig"] + +[build-system] +requires = ["poetry-core"] +build-backend = "poetry.core.masonry.api" +"#, + )?; + + uv_snapshot!(context.install() + .arg("-r") + .arg("pyproject.toml") + .arg("--extra") + .arg("test"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Downloaded 4 packages in [TIME] + Installed 4 packages in [TIME] + + anyio==3.7.1 + + idna==3.6 + + iniconfig==2.0.0 + + sniffio==1.3.1 + "### + ); + + Ok(()) +} + /// Respect installed versions when resolving. #[test] fn respect_installed_and_reinstall() -> Result<()> { From e0d55ef496126129a46f0f0f4e59814a3e534d2c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 3 Apr 2024 16:36:44 -0400 Subject: [PATCH 002/110] Allow no-op `--no-compile` flag on CLI (#2816) Closes https://github.com/astral-sh/uv/issues/2771. --- crates/uv/src/main.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 97511eb4d..ab8d4d71f 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -677,6 +677,10 @@ struct PipSyncArgs { #[clap(long)] compile: bool, + /// Don't compile Python files to bytecode. + #[clap(long, hide = true, conflicts_with = "compile")] + no_compile: bool, + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[clap(long, short = 'C', alias = "config-settings")] config_setting: Vec, @@ -938,6 +942,10 @@ struct PipInstallArgs { #[clap(long)] compile: bool, + /// Don't compile Python files to bytecode. + #[clap(long, hide = true, conflicts_with = "compile")] + no_compile: bool, + /// Settings to pass to the PEP 517 build backend, specified as `KEY=VALUE` pairs. #[clap(long, short = 'C', alias = "config-settings")] config_setting: Vec, From 34341bd6e9baff45342b75cf24861ce84725f8c4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 3 Apr 2024 19:23:37 -0400 Subject: [PATCH 003/110] Allow package lookups across multiple indexes via explicit opt-in (#2815) ## Summary This partially revives https://github.com/astral-sh/uv/pull/2135 (with some modifications) to enable users to opt-in to looking for packages across multiple indexes. The behavior is such that, in version selection, we take _any_ compatible version from a "higher-priority" index over the compatible versions of a "lower-priority" index, even if that means we might accept an "older" version. Closes https://github.com/astral-sh/uv/issues/2775. --- Cargo.lock | 2 + PIP_COMPATIBILITY.md | 6 ++ README.md | 3 + crates/uv-client/Cargo.toml | 1 + crates/uv-client/src/registry_client.rs | 46 +++++--- crates/uv-dev/src/resolve_many.rs | 15 ++- crates/uv-resolver/src/candidate_selector.rs | 55 ++++++---- crates/uv-resolver/src/error.rs | 17 +-- crates/uv-resolver/src/resolution.rs | 28 ++--- crates/uv-resolver/src/resolver/mod.rs | 17 ++- crates/uv-resolver/src/resolver/provider.rs | 39 ++++--- crates/uv-resolver/src/version_map.rs | 12 +-- crates/uv-types/Cargo.toml | 1 + crates/uv-types/src/build_options.rs | 23 ++++ crates/uv/Cargo.toml | 2 +- crates/uv/src/commands/pip_compile.rs | 6 +- crates/uv/src/commands/pip_install.rs | 6 +- crates/uv/src/commands/pip_sync.rs | 6 +- crates/uv/src/commands/venv.rs | 7 +- crates/uv/src/logging.rs | 2 +- crates/uv/src/main.rs | 42 +++++++- crates/uv/tests/common/mod.rs | 14 ++- crates/uv/tests/pip_compile.rs | 107 +++++++++++++++++++ 23 files changed, 351 insertions(+), 106 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b8cba8cb4..0395e2662 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4464,6 +4464,7 @@ dependencies = [ "uv-cache", "uv-fs", "uv-normalize", + "uv-types", "uv-version", "uv-warnings", "webpki-roots", @@ -4785,6 +4786,7 @@ name = "uv-types" version = "0.0.1" dependencies = [ "anyhow", + "clap", "distribution-types", "itertools 0.12.1", "once-map", diff --git a/PIP_COMPATIBILITY.md b/PIP_COMPATIBILITY.md index 413f117c6..77bde9115 100644 --- a/PIP_COMPATIBILITY.md +++ b/PIP_COMPATIBILITY.md @@ -128,6 +128,12 @@ internal package, thus causing the malicious package to be installed instead of package. See, for example, [the `torchtriton` attack](https://pytorch.org/blog/compromised-nightly-dependency/) from December 2022. +As of v0.1.29, users can opt in to `pip`-style behavior for multiple indexes via the +`--index-strategy unsafe-any-match` command-line option, or the `UV_INDEX_STRATEGY` environment +variable. When enabled, uv will search for each package across all indexes, and consider all +available versions when resolving dependencies, prioritizing the `--extra-index-url` indexes over +the default index URL. (Versions that are duplicated _across_ indexes will be ignored.) + In the future, uv will support pinning packages to dedicated indexes (see: [#171](https://github.com/astral-sh/uv/issues/171)). Additionally, [PEP 708](https://peps.python.org/pep-0708/) is a provisional standard that aims to address the "dependency confusion" issue across package registries and installers. diff --git a/README.md b/README.md index 8bba8c9fb..a00d547bb 100644 --- a/README.md +++ b/README.md @@ -449,6 +449,9 @@ uv accepts the following command-line arguments as environment variables: should be used with caution, as it can modify the system Python installation. - `UV_NATIVE_TLS`: Equivalent to the `--native-tls` command-line argument. If set to `true`, uv will use the system's trust store instead of the bundled `webpki-roots` crate. +- `UV_INDEX_STRATEGY`: Equivalent to the `--index-strategy` command-line argument. For example, if + set to `unsafe-any-match`, uv will consider versions of a given package available across all + index URLs, rather than limiting its search to the first index URL that contains the package. In each case, the corresponding command-line argument takes precedence over an environment variable. diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 0d89d290b..8bcdf8e5d 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -15,6 +15,7 @@ uv-auth = { workspace = true } uv-cache = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-normalize = { workspace = true } +uv-types = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } pypi-types = { workspace = true } diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 8ae51f6b2..ec9bbf572 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -23,6 +23,7 @@ use pypi_types::{Metadata23, SimpleJson}; use uv_auth::KeyringProvider; use uv_cache::{Cache, CacheBucket, WheelCache}; use uv_normalize::PackageName; +use uv_types::IndexStrategy; use crate::base_client::{BaseClient, BaseClientBuilder}; use crate::cached_client::CacheControl; @@ -35,6 +36,7 @@ use crate::{CachedClient, CachedClientError, Error, ErrorKind}; #[derive(Debug, Clone)] pub struct RegistryClientBuilder<'a> { index_urls: IndexUrls, + index_strategy: IndexStrategy, keyring_provider: KeyringProvider, native_tls: bool, retries: u32, @@ -49,6 +51,7 @@ impl RegistryClientBuilder<'_> { pub fn new(cache: Cache) -> Self { Self { index_urls: IndexUrls::default(), + index_strategy: IndexStrategy::default(), keyring_provider: KeyringProvider::default(), native_tls: false, cache, @@ -68,6 +71,12 @@ impl<'a> RegistryClientBuilder<'a> { self } + #[must_use] + pub fn index_strategy(mut self, index_strategy: IndexStrategy) -> Self { + self.index_strategy = index_strategy; + self + } + #[must_use] pub fn keyring_provider(mut self, keyring_provider: KeyringProvider) -> Self { self.keyring_provider = keyring_provider; @@ -147,6 +156,7 @@ impl<'a> RegistryClientBuilder<'a> { RegistryClient { index_urls: self.index_urls, + index_strategy: self.index_strategy, cache: self.cache, connectivity, client, @@ -160,6 +170,8 @@ impl<'a> RegistryClientBuilder<'a> { pub struct RegistryClient { /// The index URLs to use for fetching packages. index_urls: IndexUrls, + /// The strategy to use when fetching across multiple indexes. + index_strategy: IndexStrategy, /// The underlying HTTP client. client: CachedClient, /// Used for the remote wheel METADATA cache. @@ -206,17 +218,23 @@ impl RegistryClient { pub async fn simple( &self, package_name: &PackageName, - ) -> Result<(IndexUrl, OwnedArchive), Error> { + ) -> Result)>, Error> { let mut it = self.index_urls.indexes().peekable(); if it.peek().is_none() { return Err(ErrorKind::NoIndex(package_name.as_ref().to_string()).into()); } + let mut results = Vec::new(); for index in it { - let result = self.simple_single_index(package_name, index).await?; + match self.simple_single_index(package_name, index).await? { + Ok(metadata) => { + results.push((index.clone(), metadata)); - return match result { - Ok(metadata) => Ok((index.clone(), metadata)), + // If we're only using the first match, we can stop here. + if self.index_strategy == IndexStrategy::FirstMatch { + break; + } + } Err(CachedClientError::Client(err)) => match err.into_kind() { ErrorKind::Offline(_) => continue, ErrorKind::ReqwestError(err) => { @@ -225,20 +243,24 @@ impl RegistryClient { { continue; } - Err(ErrorKind::from(err).into()) + return Err(ErrorKind::from(err).into()); } - other => Err(other.into()), + other => return Err(other.into()), }, - Err(CachedClientError::Callback(err)) => Err(err), + Err(CachedClientError::Callback(err)) => return Err(err), }; } - match self.connectivity { - Connectivity::Online => { - Err(ErrorKind::PackageNotFound(package_name.to_string()).into()) - } - Connectivity::Offline => Err(ErrorKind::Offline(package_name.to_string()).into()), + if results.is_empty() { + return match self.connectivity { + Connectivity::Online => { + Err(ErrorKind::PackageNotFound(package_name.to_string()).into()) + } + Connectivity::Offline => Err(ErrorKind::Offline(package_name.to_string()).into()), + }; } + + Ok(results) } async fn simple_single_index( diff --git a/crates/uv-dev/src/resolve_many.rs b/crates/uv-dev/src/resolve_many.rs index f5608b46a..bd0e53b1c 100644 --- a/crates/uv-dev/src/resolve_many.rs +++ b/crates/uv-dev/src/resolve_many.rs @@ -47,10 +47,17 @@ async fn find_latest_version( client: &RegistryClient, package_name: &PackageName, ) -> Option { - let (_, raw_simple_metadata) = client.simple(package_name).await.ok()?; - let simple_metadata = OwnedArchive::deserialize(&raw_simple_metadata); - let version = simple_metadata.into_iter().next()?.version; - Some(version) + client + .simple(package_name) + .await + .ok() + .into_iter() + .flatten() + .filter_map(|(_index, raw_simple_metadata)| { + let simple_metadata = OwnedArchive::deserialize(&raw_simple_metadata); + Some(simple_metadata.into_iter().next()?.version) + }) + .max() } pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> { diff --git a/crates/uv-resolver/src/candidate_selector.rs b/crates/uv-resolver/src/candidate_selector.rs index eff292bd9..33a20d73d 100644 --- a/crates/uv-resolver/src/candidate_selector.rs +++ b/crates/uv-resolver/src/candidate_selector.rs @@ -71,7 +71,7 @@ impl CandidateSelector { &'a self, package_name: &'a PackageName, range: &'a Range, - version_map: &'a VersionMap, + version_maps: &'a [VersionMap], preferences: &'a Preferences, installed_packages: &'a InstalledPackages, exclusions: &'a Exclusions, @@ -107,7 +107,10 @@ impl CandidateSelector { } // Check for a remote distribution that matches the preferred version - if let Some(file) = version_map.get(version) { + if let Some(file) = version_maps + .iter() + .find_map(|version_map| version_map.get(version)) + { return Some(Candidate::new(package_name, version, file)); } } @@ -163,33 +166,39 @@ impl CandidateSelector { "selecting candidate for package {:?} with range {:?} with {} remote versions", package_name, range, - version_map.len() + version_maps.iter().map(VersionMap::len).sum::(), ); match &self.resolution_strategy { - ResolutionStrategy::Highest => Self::select_candidate( - version_map.iter().rev(), - package_name, - range, - allow_prerelease, - ), - ResolutionStrategy::Lowest => { + ResolutionStrategy::Highest => version_maps.iter().find_map(|version_map| { + Self::select_candidate( + version_map.iter().rev(), + package_name, + range, + allow_prerelease, + ) + }), + ResolutionStrategy::Lowest => version_maps.iter().find_map(|version_map| { Self::select_candidate(version_map.iter(), package_name, range, allow_prerelease) - } + }), ResolutionStrategy::LowestDirect(direct_dependencies) => { if direct_dependencies.contains(package_name) { - Self::select_candidate( - version_map.iter(), - package_name, - range, - allow_prerelease, - ) + version_maps.iter().find_map(|version_map| { + Self::select_candidate( + version_map.iter(), + package_name, + range, + allow_prerelease, + ) + }) } else { - Self::select_candidate( - version_map.iter().rev(), - package_name, - range, - allow_prerelease, - ) + version_maps.iter().find_map(|version_map| { + Self::select_candidate( + version_map.iter().rev(), + package_name, + range, + allow_prerelease, + ) + }) } } } diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 6cf92bf4c..96c46fcb1 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -209,14 +209,15 @@ impl NoSolutionError { // we represent the state of the resolver at the time of failure. if visited.contains(name) { if let Some(response) = package_versions.get(name) { - if let VersionsResponse::Found(ref version_map) = *response { - available_versions.insert( - package.clone(), - version_map - .iter() - .map(|(version, _)| version.clone()) - .collect(), - ); + if let VersionsResponse::Found(ref version_maps) = *response { + for version_map in version_maps { + available_versions + .entry(package.clone()) + .or_insert_with(BTreeSet::new) + .extend( + version_map.iter().map(|(version, _)| version.clone()), + ); + } } } } diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index 667f79154..b1c29d34f 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -99,12 +99,14 @@ impl ResolutionGraph { if let Some(hash) = preferences.match_hashes(package_name, version) { hashes.insert(package_name.clone(), hash.to_vec()); } else if let Some(versions_response) = packages.get(package_name) { - if let VersionsResponse::Found(ref version_map) = *versions_response { - hashes.insert(package_name.clone(), { - let mut hash = version_map.hashes(version); - hash.sort_unstable(); - hash - }); + if let VersionsResponse::Found(ref version_maps) = *versions_response { + for version_map in version_maps { + if let Some(mut hash) = version_map.hashes(version) { + hash.sort_unstable(); + hashes.insert(package_name.clone(), hash); + break; + } + } } } @@ -127,12 +129,14 @@ impl ResolutionGraph { if let Some(hash) = preferences.match_hashes(package_name, version) { hashes.insert(package_name.clone(), hash.to_vec()); } else if let Some(versions_response) = packages.get(package_name) { - if let VersionsResponse::Found(ref version_map) = *versions_response { - hashes.insert(package_name.clone(), { - let mut hash = version_map.hashes(version); - hash.sort_unstable(); - hash - }); + if let VersionsResponse::Found(ref version_maps) = *versions_response { + for version_map in version_maps { + if let Some(mut hash) = version_map.hashes(version) { + hash.sort_unstable(); + hashes.insert(package_name.clone(), hash); + break; + } + } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 5b99e307d..379a18c0c 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -6,7 +6,6 @@ use std::ops::Deref; use std::sync::Arc; use anyhow::Result; - use dashmap::{DashMap, DashSet}; use futures::{FutureExt, StreamExt}; use itertools::Itertools; @@ -34,7 +33,6 @@ use uv_normalize::PackageName; use uv_types::{BuildContext, Constraints, InstalledPackagesProvider, Overrides}; use crate::candidate_selector::{CandidateDist, CandidateSelector}; - use crate::editables::Editables; use crate::error::ResolveError; use crate::manifest::Manifest; @@ -54,7 +52,7 @@ pub use crate::resolver::provider::{ use crate::resolver::reporter::Facade; pub use crate::resolver::reporter::{BuildId, Reporter}; use crate::yanks::AllowedYanks; -use crate::{DependencyMode, Exclusions, Options, VersionMap}; +use crate::{DependencyMode, Exclusions, Options}; mod index; mod locals; @@ -632,23 +630,22 @@ impl< .ok_or(ResolveError::Unregistered)?; self.visited.insert(package_name.clone()); - let empty_version_map = VersionMap::default(); - let version_map = match *versions_response { - VersionsResponse::Found(ref version_map) => version_map, + let version_maps = match *versions_response { + VersionsResponse::Found(ref version_maps) => version_maps.as_slice(), VersionsResponse::NoIndex => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::NoIndex); - &empty_version_map + &[] } VersionsResponse::Offline => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::Offline); - &empty_version_map + &[] } VersionsResponse::NotFound => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::NotFound); - &empty_version_map + &[] } }; @@ -664,7 +661,7 @@ impl< let Some(candidate) = self.selector.select( package_name, range, - version_map, + version_maps, &self.preferences, self.installed_packages, &self.exclusions, diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 266f7fc1e..273fc2b53 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -22,7 +22,7 @@ pub type WheelMetadataResult = Result; #[derive(Debug)] pub enum VersionsResponse { /// The package was found in the registry with the included versions - Found(VersionMap), + Found(Vec), /// The package was not found in the registry NotFound, /// The package was not found in the local registry @@ -113,29 +113,36 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider // If the "Simple API" request was successful, convert to `VersionMap` on the Tokio // threadpool, since it can be slow. match result { - Ok((index, metadata)) => Ok(VersionsResponse::Found(VersionMap::from_metadata( - metadata, - package_name, - &index, - &self.tags, - &self.python_requirement, - &self.allowed_yanks, - self.exclude_newer.as_ref(), - self.flat_index.get(package_name).cloned(), - &self.no_binary, - &self.no_build, - ))), + Ok(results) => Ok(VersionsResponse::Found( + results + .into_iter() + .map(|(index, metadata)| { + VersionMap::from_metadata( + metadata, + package_name, + &index, + &self.tags, + &self.python_requirement, + &self.allowed_yanks, + self.exclude_newer.as_ref(), + self.flat_index.get(package_name).cloned(), + &self.no_binary, + &self.no_build, + ) + }) + .collect(), + )), Err(err) => match err.into_kind() { uv_client::ErrorKind::PackageNotFound(_) => { if let Some(flat_index) = self.flat_index.get(package_name).cloned() { - Ok(VersionsResponse::Found(VersionMap::from(flat_index))) + Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)])) } else { Ok(VersionsResponse::NotFound) } } uv_client::ErrorKind::NoIndex(_) => { if let Some(flat_index) = self.flat_index.get(package_name).cloned() { - Ok(VersionsResponse::Found(VersionMap::from(flat_index))) + Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)])) } else if self.flat_index.offline() { Ok(VersionsResponse::Offline) } else { @@ -144,7 +151,7 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider } uv_client::ErrorKind::Offline(_) => { if let Some(flat_index) = self.flat_index.get(package_name).cloned() { - Ok(VersionsResponse::Found(VersionMap::from(flat_index))) + Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)])) } else { Ok(VersionsResponse::Offline) } diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index b3045e600..c7937bef8 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -174,16 +174,10 @@ impl VersionMap { } /// Return the [`Hashes`] for the given version, if any. - pub(crate) fn hashes(&self, version: &Version) -> Vec { + pub(crate) fn hashes(&self, version: &Version) -> Option> { match self.inner { - VersionMapInner::Eager(ref map) => map - .get(version) - .map(|file| file.hashes().to_vec()) - .unwrap_or_default(), - VersionMapInner::Lazy(ref lazy) => lazy - .get(version) - .map(|file| file.hashes().to_vec()) - .unwrap_or_default(), + VersionMapInner::Eager(ref map) => map.get(version).map(|file| file.hashes().to_vec()), + VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(|file| file.hashes().to_vec()), } } diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index e6210fb49..4fe6ccb29 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -21,6 +21,7 @@ uv-interpreter = { workspace = true } uv-normalize = { workspace = true } anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"], optional = true } itertools = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, optional = true } diff --git a/crates/uv-types/src/build_options.rs b/crates/uv-types/src/build_options.rs index 81b646576..bcdaee92b 100644 --- a/crates/uv-types/src/build_options.rs +++ b/crates/uv-types/src/build_options.rs @@ -211,6 +211,29 @@ impl NoBuild { } } +#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)] +#[cfg_attr(feature = "clap", derive(clap::ValueEnum))] +pub enum IndexStrategy { + /// Only use results from the first index that returns a match for a given package name. + /// + /// While this differs from pip's behavior, it's the default index strategy as it's the most + /// secure. + #[default] + FirstMatch, + /// Search for every package name across all indexes, exhausting the versions from the first + /// index before moving on to the next. + /// + /// In 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). + /// + /// See: https://peps.python.org/pep-0708/ + UnsafeAnyMatch, +} + #[cfg(test)] mod tests { use std::str::FromStr; diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 1dba92d0a..2fde36bcf 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -31,7 +31,7 @@ uv-interpreter = { workspace = true } uv-normalize = { workspace = true } uv-requirements = { workspace = true } uv-resolver = { workspace = true, features = ["clap"] } -uv-types = { workspace = true } +uv-types = { workspace = true, features = ["clap"] } uv-virtualenv = { workspace = true } uv-warnings = { workspace = true } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 28c28978c..31681e883 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -36,8 +36,8 @@ use uv_resolver::{ OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, }; use uv_types::{ - BuildIsolation, ConfigSettings, Constraints, EmptyInstalledPackages, InFlight, NoBinary, - NoBuild, Overrides, SetupPyStrategy, Upgrade, + BuildIsolation, ConfigSettings, Constraints, EmptyInstalledPackages, InFlight, IndexStrategy, + NoBinary, NoBuild, Overrides, SetupPyStrategy, Upgrade, }; use uv_warnings::warn_user; @@ -67,6 +67,7 @@ pub(crate) async fn pip_compile( include_find_links: bool, include_marker_expression: bool, index_locations: IndexLocations, + index_strategy: IndexStrategy, keyring_provider: KeyringProvider, setup_py: SetupPyStrategy, config_settings: ConfigSettings, @@ -210,6 +211,7 @@ pub(crate) async fn pip_compile( .native_tls(native_tls) .connectivity(connectivity) .index_urls(index_locations.index_urls()) + .index_strategy(index_strategy) .keyring_provider(keyring_provider) .markers(&markers) .platform(interpreter.platform()) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 0dc96ad05..ba11f3f7a 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -38,8 +38,8 @@ use uv_resolver::{ Preference, ResolutionGraph, ResolutionMode, Resolver, }; use uv_types::{ - BuildIsolation, ConfigSettings, Constraints, InFlight, NoBinary, NoBuild, Overrides, Reinstall, - SetupPyStrategy, Upgrade, + BuildIsolation, ConfigSettings, Constraints, InFlight, IndexStrategy, NoBinary, NoBuild, + Overrides, Reinstall, SetupPyStrategy, Upgrade, }; use uv_warnings::warn_user; @@ -61,6 +61,7 @@ pub(crate) async fn pip_install( dependency_mode: DependencyMode, upgrade: Upgrade, index_locations: IndexLocations, + index_strategy: IndexStrategy, keyring_provider: KeyringProvider, reinstall: Reinstall, link_mode: LinkMode, @@ -195,6 +196,7 @@ pub(crate) async fn pip_install( .native_tls(native_tls) .connectivity(connectivity) .index_urls(index_locations.index_urls()) + .index_strategy(index_strategy) .keyring_provider(keyring_provider) .markers(markers) .platform(interpreter.platform()) diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index acc8ac8df..c5f11d73f 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -28,8 +28,8 @@ use uv_requirements::{ }; use uv_resolver::{DependencyMode, InMemoryIndex, Manifest, OptionsBuilder, Resolver}; use uv_types::{ - BuildIsolation, ConfigSettings, EmptyInstalledPackages, InFlight, NoBinary, NoBuild, Reinstall, - SetupPyStrategy, + BuildIsolation, ConfigSettings, EmptyInstalledPackages, InFlight, IndexStrategy, NoBinary, + NoBuild, Reinstall, SetupPyStrategy, }; use uv_warnings::warn_user; @@ -45,6 +45,7 @@ pub(crate) async fn pip_sync( link_mode: LinkMode, compile: bool, index_locations: IndexLocations, + index_strategy: IndexStrategy, keyring_provider: KeyringProvider, setup_py: SetupPyStrategy, connectivity: Connectivity, @@ -144,6 +145,7 @@ pub(crate) async fn pip_sync( .native_tls(native_tls) .connectivity(connectivity) .index_urls(index_locations.index_urls()) + .index_strategy(index_strategy) .keyring_provider(keyring_provider) .markers(venv.interpreter().markers()) .platform(venv.interpreter().platform()) diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 6faa5e131..e6c842cbd 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -21,7 +21,8 @@ use uv_fs::Simplified; use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_resolver::{InMemoryIndex, OptionsBuilder}; use uv_types::{ - BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBinary, NoBuild, SetupPyStrategy, + BuildContext, BuildIsolation, ConfigSettings, InFlight, IndexStrategy, NoBinary, NoBuild, + SetupPyStrategy, }; use crate::commands::ExitStatus; @@ -34,6 +35,7 @@ pub(crate) async fn venv( path: &Path, python_request: Option<&str>, index_locations: &IndexLocations, + index_strategy: IndexStrategy, keyring_provider: KeyringProvider, prompt: uv_virtualenv::Prompt, system_site_packages: bool, @@ -48,6 +50,7 @@ pub(crate) async fn venv( path, python_request, index_locations, + index_strategy, keyring_provider, prompt, system_site_packages, @@ -93,6 +96,7 @@ async fn venv_impl( path: &Path, python_request: Option<&str>, index_locations: &IndexLocations, + index_strategy: IndexStrategy, keyring_provider: KeyringProvider, prompt: uv_virtualenv::Prompt, system_site_packages: bool, @@ -150,6 +154,7 @@ async fn venv_impl( let client = RegistryClientBuilder::new(cache.clone()) .native_tls(native_tls) .index_urls(index_locations.index_urls()) + .index_strategy(index_strategy) .keyring_provider(keyring_provider) .connectivity(connectivity) .markers(interpreter.markers()) diff --git a/crates/uv/src/logging.rs b/crates/uv/src/logging.rs index 67c491a4d..6487b0fb6 100644 --- a/crates/uv/src/logging.rs +++ b/crates/uv/src/logging.rs @@ -124,7 +124,7 @@ pub(crate) fn setup_logging( } Level::Verbose | Level::ExtraVerbose => { // Show `DEBUG` messages from the CLI crate, but allow `RUST_LOG` to override. - Directive::from_str("uv=debug").unwrap() + Directive::from_str("uv=trace").unwrap() } }; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index ab8d4d71f..913069bf4 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -20,11 +20,11 @@ use uv_interpreter::PythonVersion; use uv_normalize::{ExtraName, PackageName}; use uv_requirements::{ExtrasSpecification, RequirementsSource}; use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode}; -use uv_types::NoBinary; use uv_types::{ ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, Reinstall, SetupPyStrategy, Upgrade, }; +use uv_types::{IndexStrategy, NoBinary}; use crate::commands::{extra_name_with_clap_error, ExitStatus, ListFormat, VersionFormat}; use crate::compat::CompatArgs; @@ -384,6 +384,15 @@ struct PipCompileArgs { #[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")] no_index: bool, + /// 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. This prevents "dependency confusion" + /// attacks, whereby an attack can upload a malicious package under the same name to a secondary + /// index. + #[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] + index_strategy: IndexStrategy, + /// Attempt to use `keyring` for authentication for index urls /// /// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently @@ -570,6 +579,15 @@ struct PipSyncArgs { #[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")] no_index: bool, + /// 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. This prevents "dependency confusion" + /// attacks, whereby an attack can upload a malicious package under the same name to a secondary + /// index. + #[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] + index_strategy: IndexStrategy, + /// Attempt to use `keyring` for authentication for index urls /// /// Function's similar to `pip`'s `--keyring-provider subprocess` argument, @@ -835,6 +853,15 @@ struct PipInstallArgs { #[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")] no_index: bool, + /// 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. This prevents "dependency confusion" + /// attacks, whereby an attack can upload a malicious package under the same name to a secondary + /// index. + #[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] + index_strategy: IndexStrategy, + /// Attempt to use `keyring` for authentication for index urls /// /// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently @@ -1336,6 +1363,15 @@ struct VenvArgs { #[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")] no_index: bool, + /// 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. This prevents "dependency confusion" + /// attacks, whereby an attack can upload a malicious package under the same name to a secondary + /// index. + #[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] + index_strategy: IndexStrategy, + /// Attempt to use `keyring` for authentication for index urls /// /// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently @@ -1552,6 +1588,7 @@ async fn run() -> Result { args.emit_find_links, args.emit_marker_expression, index_urls, + args.index_strategy, args.keyring_provider, setup_py, config_settings, @@ -1608,6 +1645,7 @@ async fn run() -> Result { args.link_mode, args.compile, index_urls, + args.index_strategy, args.keyring_provider, setup_py, if args.offline { @@ -1701,6 +1739,7 @@ async fn run() -> Result { dependency_mode, upgrade, index_urls, + args.index_strategy, args.keyring_provider, reinstall, args.link_mode, @@ -1833,6 +1872,7 @@ async fn run() -> Result { &args.name, args.python.as_deref(), &index_locations, + args.index_strategy, args.keyring_provider, uv_virtualenv::Prompt::from_args(prompt), args.system_site_packages, diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index f9f384165..4d09b7ac7 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -139,13 +139,23 @@ impl TestContext { /// * Set a cutoff for versions used in the resolution so the snapshots don't change after a new release. /// * Set the venv to a fresh `.venv` in `temp_dir`. pub fn compile(&self) -> std::process::Command { + let mut command = self.compile_without_exclude_newer(); + command.arg("--exclude-newer").arg(EXCLUDE_NEWER); + command + } + + /// Create a `pip compile` command with no `--exclude-newer` option. + /// + /// One should avoid using this in tests to the extent possible because + /// it can result in tests failing when the index state changes. Therefore, + /// if you use this, there should be some other kind of mitigation in place. + /// For example, pinning package versions. + pub fn compile_without_exclude_newer(&self) -> std::process::Command { let mut cmd = std::process::Command::new(get_bin()); cmd.arg("pip") .arg("compile") .arg("--cache-dir") .arg(self.cache_dir.path()) - .arg("--exclude-newer") - .arg(EXCLUDE_NEWER) .env("VIRTUAL_ENV", self.venv.as_os_str()) .current_dir(self.temp_dir.path()); diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 2debbb05b..4cca0a55d 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -7040,3 +7040,110 @@ requires-python = ">3.8" Ok(()) } + +/// Install a package via `--extra-index-url`. +/// +/// If the package exists exist on the "extra" index, but at an incompatible version, the +/// resolution should fail by default (even though a compatible version exists on the "primary" +/// index). +#[test] +fn compile_index_url_first_match() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("jinja2==3.1.0")?; + + uv_snapshot!(context.compile() + .arg("--index-url") + .arg("https://pypi.org/simple") + .arg("--extra-index-url") + .arg("https://download.pytorch.org/whl/cpu") + .arg("requirements.in") + .arg("--no-deps"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because there is no version of jinja2==3.1.0 and you require + jinja2==3.1.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Install a package via `--extra-index-url`. +/// +/// If the package exists exist on the "extra" index, but at an incompatible version, the +/// resolution should fallback to the "primary" index when `--index-strategy unsafe-any-match` +/// is provided. +#[test] +fn compile_index_url_fallback() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("jinja2==3.1.0")?; + + uv_snapshot!(context.compile() + .arg("--index-strategy") + .arg("unsafe-any-match") + .arg("--index-url") + .arg("https://pypi.org/simple") + .arg("--extra-index-url") + .arg("https://download.pytorch.org/whl/cpu") + .arg("requirements.in") + .arg("--no-deps"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z --index-strategy unsafe-any-match requirements.in --no-deps + jinja2==3.1.0 + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} + +/// Install a package via `--extra-index-url`. +/// +/// If the package exists exist on the "extra" index at a compatible version, the resolver should +/// prefer it, even if a newer versions exists on the "primary" index. +/// +/// In this case, Jinja 3.1.2 is hosted on the "extra" index, but newer versions are available on +/// the "primary" index. +#[test] +fn compile_index_url_fallback_prefer_primary() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("jinja2")?; + + uv_snapshot!(context.compile_without_exclude_newer() + .arg("--index-strategy") + .arg("unsafe-any-match") + .arg("--index-url") + .arg("https://pypi.org/simple") + .arg("--extra-index-url") + .arg("https://download.pytorch.org/whl/cpu") + .arg("requirements.in") + .arg("--no-deps"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --index-strategy unsafe-any-match requirements.in --no-deps + jinja2==3.1.2 + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} From dc2c289dfff2e7cae6b4417c1d9661adf488c2ce Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 3 Apr 2024 21:31:40 -0400 Subject: [PATCH 004/110] Upgrade `rs-async-zip` to support data descriptors (#2809) ## Summary Upgrading `rs-async-zip` enables us to support data descriptors in streaming. This both greatly improves performance for indexes that use data descriptors _and_ ensures that we support them in a few other places (e.g., zipped source distributions created in Finder). Closes #2808. --- Cargo.lock | 4 ++-- Cargo.toml | 2 +- crates/uv-client/src/registry_client.rs | 6 ++++-- crates/uv-client/src/remote_metadata.rs | 6 ++++-- crates/uv-distribution/src/distribution_database.rs | 3 +-- crates/uv-distribution/src/source/mod.rs | 3 +-- crates/uv-extract/src/seek.rs | 4 ++-- crates/uv-extract/src/stream.rs | 10 ++++------ 8 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0395e2662..7a4676e40 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -246,8 +246,8 @@ dependencies = [ [[package]] name = "async_zip" -version = "0.0.16" -source = "git+https://github.com/charliermarsh/rs-async-zip?rev=d76801da0943de985254fc6255c0e476b57c5836#d76801da0943de985254fc6255c0e476b57c5836" +version = "0.0.17" +source = "git+https://github.com/charliermarsh/rs-async-zip?rev=1dcb40cfe1bf5325a6fd4bfcf9894db40241f585#1dcb40cfe1bf5325a6fd4bfcf9894db40241f585" dependencies = [ "async-compression", "crc32fast", diff --git a/Cargo.toml b/Cargo.toml index 1b05003d5..3621f00fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ async-channel = { version = "2.2.0" } async-compression = { version = "0.4.6" } async-trait = { version = "0.1.78" } async_http_range_reader = { version = "0.7.0" } -async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "d76801da0943de985254fc6255c0e476b57c5836", features = ["deflate"] } +async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "1dcb40cfe1bf5325a6fd4bfcf9894db40241f585", features = ["deflate"] } axoupdater = { version = "0.3.1", default-features = false } backoff = { version = "0.4.0" } base64 = { version = "0.21.7" } diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index ec9bbf572..b5f51d76e 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -9,7 +9,7 @@ use http::HeaderMap; use reqwest::{Client, Response, StatusCode}; use serde::{Deserialize, Serialize}; use tokio::io::AsyncReadExt; -use tokio_util::compat::FuturesAsyncReadCompatExt; +use tokio_util::compat::{FuturesAsyncReadCompatExt, TokioAsyncReadCompatExt}; use tracing::{info_span, instrument, trace, warn, Instrument}; use url::Url; @@ -618,7 +618,8 @@ async fn read_metadata_async_seek( debug_source: String, reader: impl tokio::io::AsyncRead + tokio::io::AsyncSeek + Unpin, ) -> Result { - let mut zip_reader = async_zip::tokio::read::seek::ZipFileReader::with_tokio(reader) + let reader = futures::io::BufReader::new(reader.compat()); + let mut zip_reader = async_zip::base::read::seek::ZipFileReader::new(reader) .await .map_err(|err| ErrorKind::Zip(filename.clone(), err))?; @@ -655,6 +656,7 @@ async fn read_metadata_async_stream( debug_source: String, reader: R, ) -> Result { + let reader = futures::io::BufReader::with_capacity(128 * 1024, reader); let mut zip = async_zip::base::read::stream::ZipFileReader::new(reader); while let Some(mut entry) = zip diff --git a/crates/uv-client/src/remote_metadata.rs b/crates/uv-client/src/remote_metadata.rs index 548968e2f..11b156c70 100644 --- a/crates/uv-client/src/remote_metadata.rs +++ b/crates/uv-client/src/remote_metadata.rs @@ -1,5 +1,5 @@ use async_http_range_reader::AsyncHttpRangeReader; -use async_zip::tokio::read::seek::ZipFileReader; +use futures::io::BufReader; use tokio_util::compat::TokioAsyncReadCompatExt; use distribution_filename::WheelFilename; @@ -61,7 +61,8 @@ pub(crate) async fn wheel_metadata_from_remote_zip( .await; // Construct a zip reader to uses the stream. - let mut reader = ZipFileReader::new(reader.compat()) + let buf = BufReader::new(reader.compat()); + let mut reader = async_zip::base::read::seek::ZipFileReader::new(buf) .await .map_err(|err| ErrorKind::Zip(filename.clone(), err))?; @@ -90,6 +91,7 @@ pub(crate) async fn wheel_metadata_from_remote_zip( reader .inner_mut() .get_mut() + .get_mut() .prefetch(offset..offset + size) .await; diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index e4b1cec23..dbe968067 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -481,8 +481,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> file.seek(io::SeekFrom::Start(0)) .await .map_err(Error::CacheWrite)?; - let reader = tokio::io::BufReader::new(file); - uv_extract::seek::unzip(reader, temp_dir.path()).await?; + uv_extract::seek::unzip(file, temp_dir.path()).await?; // Persist the temporary directory to the directory store. let archive = self diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 2ff2873e9..4be6443d4 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1293,8 +1293,7 @@ async fn extract_archive(path: &Path, cache: &Cache) -> Result( target: impl AsRef, ) -> Result<(), Error> { let target = target.as_ref(); - let mut reader = reader.compat(); + let mut reader = futures::io::BufReader::new(reader.compat()); let mut zip = async_zip::base::read::seek::ZipFileReader::new(&mut reader).await?; let mut directories = FxHashSet::default(); @@ -81,7 +81,7 @@ pub async fn unzip( } /// Unzip a `.zip` or `.tar.gz` archive into the target directory, requiring `Seek`. -pub async fn archive( +pub async fn archive( reader: R, source: impl AsRef, target: impl AsRef, diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index 3754064a7..7fb4707fc 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -66,10 +66,7 @@ pub async fn unzip( use std::fs::Permissions; use std::os::unix::fs::PermissionsExt; - // To avoid lots of small reads to `reader` when parsing the central directory, wrap it in - // a buffer. - let mut buf = futures::io::BufReader::new(reader); - let mut directory = async_zip::base::read::cd::CentralDirectoryReader::new(&mut buf); + let mut directory = async_zip::base::read::cd::CentralDirectoryReader::new(&mut reader); while let Some(entry) = directory.next().await? { if entry.dir()? { continue; @@ -154,10 +151,11 @@ async fn untar_in>( /// Unzip a `.tar.gz` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. -pub async fn untar( +pub async fn untar( reader: R, target: impl AsRef, ) -> Result<(), Error> { + let reader = tokio::io::BufReader::new(reader); let decompressed_bytes = async_compression::tokio::bufread::GzipDecoder::new(reader); let mut archive = tokio_tar::ArchiveBuilder::new(decompressed_bytes) .set_preserve_mtime(false) @@ -166,7 +164,7 @@ pub async fn untar( } /// Unzip a `.zip` or `.tar.gz` archive into the target directory, without requiring `Seek`. -pub async fn archive( +pub async fn archive( reader: R, source: impl AsRef, target: impl AsRef, From dd3009ad84fb8a1cc1f002218ad332595a1123d6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 3 Apr 2024 22:05:54 -0400 Subject: [PATCH 005/110] Respect Git tags and branches that look like short commits (#2795) ## Summary If we're given a Git reference like `20240222`, we currently treat it as a short commit hash. However... it _could_ be a branch or a tag. This PR improves the Git reference logic to ensure that ambiguous references like `20240222` are handled appropriately, by attempting to extract it as a branch, then a tag, then a short commit hash. Closes https://github.com/astral-sh/uv/issues/2772. --- crates/uv-git/src/git.rs | 209 +++++++++++++-------------- crates/uv-git/src/lib.rs | 8 +- crates/uv/tests/pip_compile.rs | 249 ++++++++++++++------------------- 3 files changed, 205 insertions(+), 261 deletions(-) diff --git a/crates/uv-git/src/git.rs b/crates/uv-git/src/git.rs index faddeefa1..cd37c9fc2 100644 --- a/crates/uv-git/src/git.rs +++ b/crates/uv-git/src/git.rs @@ -25,20 +25,14 @@ const CHECKOUT_READY_LOCK: &str = ".ok"; /// A reference to commit or commit-ish. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum GitReference { - /// From a branch. - #[allow(unused)] - Branch(String), - /// From a tag. - #[allow(unused)] - Tag(String), /// From a reference that's ambiguously a branch or tag. BranchOrTag(String), + /// From a reference that's ambiguously a short commit, a branch, or a tag. + BranchOrTagOrCommit(String), + /// From a named reference, like `refs/pull/493/head`. + NamedRef(String), /// From a specific revision, using a full 40-character commit hash. FullCommit(String), - /// From a truncated revision. - ShortCommit(String), - /// From a named reference, like `refs/pull/493/head`. - Ref(String), /// The default branch of the repository, the reference named `HEAD`. DefaultBranch, } @@ -52,37 +46,28 @@ enum RefspecStrategy { } impl GitReference { + /// Creates a [`GitReference`] from a revision string. pub(crate) fn from_rev(rev: &str) -> Self { if rev.starts_with("refs/") { - Self::Ref(rev.to_owned()) + Self::NamedRef(rev.to_owned()) } else if looks_like_commit_hash(rev) { if rev.len() == 40 { Self::FullCommit(rev.to_owned()) } else { - Self::ShortCommit(rev.to_owned()) + Self::BranchOrTagOrCommit(rev.to_owned()) } } else { Self::BranchOrTag(rev.to_owned()) } } - pub fn precise(&self) -> Option<&str> { - match self { - Self::FullCommit(rev) => Some(rev), - Self::ShortCommit(rev) => Some(rev), - _ => None, - } - } - /// Converts the [`GitReference`] to a `str`. pub fn as_str(&self) -> Option<&str> { match self { - Self::Branch(rev) => Some(rev), - Self::Tag(rev) => Some(rev), Self::BranchOrTag(rev) => Some(rev), Self::FullCommit(rev) => Some(rev), - Self::ShortCommit(rev) => Some(rev), - Self::Ref(rev) => Some(rev), + Self::BranchOrTagOrCommit(rev) => Some(rev), + Self::NamedRef(rev) => Some(rev), Self::DefaultBranch => None, } } @@ -90,12 +75,10 @@ impl GitReference { /// Converts the [`GitReference`] to a `str` that can be used as a revision. pub(crate) fn as_rev(&self) -> &str { match self { - Self::Branch(rev) - | Self::Tag(rev) - | Self::BranchOrTag(rev) + Self::BranchOrTag(rev) | Self::FullCommit(rev) - | Self::ShortCommit(rev) - | Self::Ref(rev) => rev, + | Self::BranchOrTagOrCommit(rev) + | Self::NamedRef(rev) => rev, Self::DefaultBranch => "HEAD", } } @@ -103,12 +86,10 @@ impl GitReference { /// Returns the kind of this reference. pub(crate) fn kind_str(&self) -> &str { match self { - Self::Branch(_) => "branch", - Self::Tag(_) => "tag", Self::BranchOrTag(_) => "branch or tag", Self::FullCommit(_) => "commit", - Self::ShortCommit(_) => "short commit", - Self::Ref(_) => "ref", + Self::BranchOrTagOrCommit(_) => "branch, tag, or commit", + Self::NamedRef(_) => "ref", Self::DefaultBranch => "default branch", } } @@ -281,37 +262,18 @@ impl GitReference { pub(crate) fn resolve(&self, repo: &git2::Repository) -> Result { let refkind = self.kind_str(); let id = match self { - // Note that we resolve the named tag here in sync with where it's - // fetched into via `fetch` below. - Self::Tag(s) => (|| -> Result { - let refname = format!("refs/remotes/origin/tags/{s}"); - let id = repo.refname_to_id(&refname)?; - let obj = repo.find_object(id, None)?; - let obj = obj.peel(ObjectType::Commit)?; - Ok(obj.id()) - })() - .with_context(|| format!("failed to find {refkind} `{s}`"))?, - - // Resolve the remote name since that's all we're configuring in - // `fetch` below. - Self::Branch(s) => { - let name = format!("origin/{s}"); - let b = repo - .find_branch(&name, git2::BranchType::Remote) - .with_context(|| format!("failed to find {refkind} `{s}`"))?; - b.get() - .target() - .ok_or_else(|| anyhow::format_err!("{refkind} `{s}` did not have a target"))? - } - // Attempt to resolve the branch, then the tag. Self::BranchOrTag(s) => { let name = format!("origin/{s}"); + // Resolve the remote name since that's all we're configuring in + // `fetch` below. repo.find_branch(&name, git2::BranchType::Remote) .ok() .and_then(|b| b.get().target()) .or_else(|| { + // Note that we resolve the named tag here in sync with where it's + // fetched into via `fetch` below. let refname = format!("refs/remotes/origin/tags/{s}"); let id = repo.refname_to_id(&refname).ok()?; let obj = repo.find_object(id, None).ok()?; @@ -321,6 +283,35 @@ impl GitReference { .ok_or_else(|| anyhow::format_err!("failed to find {refkind} `{s}`"))? } + // Attempt to resolve the branch, then the tag, then the commit. + Self::BranchOrTagOrCommit(s) => { + let name = format!("origin/{s}"); + + // Resolve the remote name since that's all we're configuring in + // `fetch` below. + repo.find_branch(&name, git2::BranchType::Remote) + .ok() + .and_then(|b| b.get().target()) + .or_else(|| { + // Note that we resolve the named tag here in sync with where it's + // fetched into via `fetch` below. + let refname = format!("refs/remotes/origin/tags/{s}"); + let id = repo.refname_to_id(&refname).ok()?; + let obj = repo.find_object(id, None).ok()?; + let obj = obj.peel(ObjectType::Commit).ok()?; + Some(obj.id()) + }) + .or_else(|| { + // Resolve the commit. + let obj = repo.revparse_single(s).ok()?; + match obj.as_tag() { + Some(tag) => Some(tag.target_id()), + None => Some(obj.id()), + } + }) + .ok_or_else(|| anyhow::format_err!("failed to find {refkind} `{s}`"))? + } + // We'll be using the HEAD commit Self::DefaultBranch => { let head_id = repo.refname_to_id("refs/remotes/origin/HEAD")?; @@ -328,7 +319,7 @@ impl GitReference { head.peel(ObjectType::Commit)?.id() } - Self::FullCommit(s) | Self::ShortCommit(s) | Self::Ref(s) => { + Self::FullCommit(s) | Self::NamedRef(s) => { let obj = repo.revparse_single(s)?; match obj.as_tag() { Some(tag) => tag.target_id(), @@ -958,14 +949,6 @@ pub(crate) fn fetch( match reference { // For branches and tags we can fetch simply one reference and copy it // locally, no need to fetch other branches/tags. - GitReference::Branch(branch) => { - refspecs.push(format!("+refs/heads/{branch}:refs/remotes/origin/{branch}")); - } - - GitReference::Tag(tag) => { - refspecs.push(format!("+refs/tags/{tag}:refs/remotes/origin/tags/{tag}")); - } - GitReference::BranchOrTag(branch_or_tag) => { refspecs.push(format!( "+refs/heads/{branch_or_tag}:refs/remotes/origin/{branch_or_tag}" @@ -976,11 +959,31 @@ pub(crate) fn fetch( refspec_strategy = RefspecStrategy::First; } + // For ambiguous references, we can fetch the exact commit (if known); otherwise, + // we fetch all branches and tags. + GitReference::BranchOrTagOrCommit(branch_or_tag_or_commit) => { + // The `oid_to_fetch` is the exact commit we want to fetch. But it could be the exact + // commit of a branch or tag. We should only fetch it directly if it's the exact commit + // of a short commit hash. + if let Some(oid_to_fetch) = + oid_to_fetch.filter(|oid| is_short_hash_of(branch_or_tag_or_commit, *oid)) + { + refspecs.push(format!("+{oid_to_fetch}:refs/commit/{oid_to_fetch}")); + } else { + // We don't know what the rev will point to. To handle this + // situation we fetch all branches and tags, and then we pray + // it's somewhere in there. + refspecs.push(String::from("+refs/heads/*:refs/remotes/origin/*")); + refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD")); + tags = true; + } + } + GitReference::DefaultBranch => { refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD")); } - GitReference::Ref(rev) => { + GitReference::NamedRef(rev) => { refspecs.push(format!("+{rev}:{rev}")); } @@ -997,19 +1000,6 @@ pub(crate) fn fetch( refspecs.push(format!("+{rev}:refs/remotes/origin/HEAD")); } } - - GitReference::ShortCommit(_) => { - if let Some(oid_to_fetch) = oid_to_fetch { - refspecs.push(format!("+{oid_to_fetch}:refs/commit/{oid_to_fetch}")); - } else { - // We don't know what the rev will point to. To handle this - // situation we fetch all branches and tags, and then we pray - // it's somewhere in there. - refspecs.push(String::from("+refs/heads/*:refs/remotes/origin/*")); - refspecs.push(String::from("+HEAD:refs/remotes/origin/HEAD")); - tags = true; - } - } } debug!("Performing a Git fetch for: {remote_url}"); @@ -1022,8 +1012,12 @@ pub(crate) fn fetch( let mut errors = refspecs .iter() .map_while(|refspec| { - let fetch_result = - fetch_with_cli(repo, remote_url, &[refspec.clone()], tags); + let fetch_result = fetch_with_cli( + repo, + remote_url, + std::slice::from_ref(refspec), + tags, + ); // Stop after the first success and log failures match fetch_result { @@ -1335,40 +1329,33 @@ fn github_fast_path( let local_object = reference.resolve(repo).ok(); let github_branch_name = match reference { - GitReference::Branch(branch) => branch, - GitReference::Tag(tag) => tag, GitReference::BranchOrTag(branch_or_tag) => branch_or_tag, GitReference::DefaultBranch => "HEAD", - GitReference::Ref(rev) => rev, - GitReference::FullCommit(rev) | GitReference::ShortCommit(rev) => { - if looks_like_commit_hash(rev) { - // `revparse_single` (used by `resolve`) is the only way to turn - // short hash -> long hash, but it also parses other things, - // like branch and tag names, which might coincidentally be - // valid hex. - // - // We only return early if `rev` is a prefix of the object found - // by `revparse_single`. Don't bother talking to GitHub in that - // case, since commit hashes are permanent. If a commit with the - // requested hash is already present in the local clone, its - // contents must be the same as what is on the server for that - // hash. - // - // If `rev` is not found locally by `revparse_single`, we'll - // need GitHub to resolve it and get a hash. If `rev` is found - // but is not a short hash of the found object, it's probably a - // branch and we also need to get a hash from GitHub, in case - // the branch has moved. - if let Some(local_object) = local_object { - if is_short_hash_of(rev, local_object) { - return Ok(FastPathRev::UpToDate); - } + GitReference::NamedRef(rev) => rev, + GitReference::FullCommit(rev) | GitReference::BranchOrTagOrCommit(rev) => { + // `revparse_single` (used by `resolve`) is the only way to turn + // short hash -> long hash, but it also parses other things, + // like branch and tag names, which might coincidentally be + // valid hex. + // + // We only return early if `rev` is a prefix of the object found + // by `revparse_single`. Don't bother talking to GitHub in that + // case, since commit hashes are permanent. If a commit with the + // requested hash is already present in the local clone, its + // contents must be the same as what is on the server for that + // hash. + // + // If `rev` is not found locally by `revparse_single`, we'll + // need GitHub to resolve it and get a hash. If `rev` is found + // but is not a short hash of the found object, it's probably a + // branch and we also need to get a hash from GitHub, in case + // the branch has moved. + if let Some(local_object) = local_object { + if is_short_hash_of(rev, local_object) { + return Ok(FastPathRev::UpToDate); } - rev - } else { - debug!("can't use github fast path with `rev = \"{}\"`", rev); - return Ok(FastPathRev::Indeterminate); } + rev } }; diff --git a/crates/uv-git/src/lib.rs b/crates/uv-git/src/lib.rs index 084571dd2..7198fd071 100644 --- a/crates/uv-git/src/lib.rs +++ b/crates/uv-git/src/lib.rs @@ -95,12 +95,10 @@ impl From for Url { } else { // Otherwise, add the branch or tag name. match git.reference { - GitReference::Branch(rev) - | GitReference::Tag(rev) - | GitReference::BranchOrTag(rev) - | GitReference::Ref(rev) + GitReference::BranchOrTag(rev) + | GitReference::NamedRef(rev) | GitReference::FullCommit(rev) - | GitReference::ShortCommit(rev) => { + | GitReference::BranchOrTagOrCommit(rev) => { url.set_path(&format!("{}@{}", url.path(), rev)); } GitReference::DefaultBranch => {} diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 4cca0a55d..a1259b62a 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -1079,13 +1079,15 @@ fn compile_sdist_url_dependency() -> Result<()> { Ok(()) } -/// Resolve a specific Flask source distribution via a Git HTTPS dependency. +/// Resolve a specific source distribution via a Git HTTPS dependency. #[test] #[cfg(feature = "git")] fn compile_git_https_dependency() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("flask @ git+https://github.com/pallets/flask.git")?; + requirements_in.write_str( + "uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage", + )?; // In addition to the standard filters, remove the `main` commit, which will change frequently. let filters: Vec<_> = [(r"@(\d|\w){40}", "@[COMMIT]")] @@ -1100,113 +1102,23 @@ fn compile_git_https_dependency() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - blinker==1.7.0 - # via flask - click==8.1.7 - # via flask - flask @ git+https://github.com/pallets/flask.git@[COMMIT] - itsdangerous==2.1.2 - # via flask - jinja2==3.1.3 - # via flask - markupsafe==2.1.5 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 - # via flask + uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@[COMMIT] ----- stderr ----- - Resolved 7 packages in [TIME] + Resolved 1 package in [TIME] "###); Ok(()) } -/// Resolve a specific Flask branch via a Git HTTPS dependency. +/// Resolve a specific branch via a Git HTTPS dependency. #[test] #[cfg(feature = "git")] fn compile_git_branch_https_dependency() -> Result<()> { - let context = TestContext::new("3.12"); - let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("flask @ git+https://github.com/pallets/flask.git@1.0.x")?; - - uv_snapshot!(context.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] --exclude-newer 2024-03-25T00:00:00Z requirements.in - click==8.1.7 - # via flask - flask @ git+https://github.com/pallets/flask.git@d92b64aa275841b0c9aea3903aba72fbc4275d91 - itsdangerous==2.1.2 - # via flask - jinja2==3.1.3 - # via flask - markupsafe==2.1.5 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 - # via flask - - ----- stderr ----- - Resolved 6 packages in [TIME] - "### - ); - - Ok(()) -} - -/// Resolve a specific Flask tag via a Git HTTPS dependency. -#[test] -#[cfg(feature = "git")] -fn compile_git_tag_https_dependency() -> Result<()> { - let context = TestContext::new("3.12"); - let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("flask @ git+https://github.com/pallets/flask.git@3.0.0")?; - - uv_snapshot!(context.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] --exclude-newer 2024-03-25T00:00:00Z requirements.in - blinker==1.7.0 - # via flask - click==8.1.7 - # via flask - flask @ git+https://github.com/pallets/flask.git@735a4701d6d5e848241e7d7535db898efb62d400 - itsdangerous==2.1.2 - # via flask - jinja2==3.1.3 - # via flask - markupsafe==2.1.5 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 - # via flask - - ----- stderr ----- - Resolved 7 packages in [TIME] - "### - ); - - Ok(()) -} - -/// Resolve a specific Flask commit via a Git HTTPS dependency. -#[test] -#[cfg(feature = "git")] -fn compile_git_long_commit_https_dependency() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str( - "flask @ git+https://github.com/pallets/flask.git@d92b64aa275841b0c9aea3903aba72fbc4275d91", + "uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@test-branch", )?; uv_snapshot!(context.compile() @@ -1216,35 +1128,25 @@ fn compile_git_long_commit_https_dependency() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - click==8.1.7 - # via flask - flask @ git+https://github.com/pallets/flask.git@d92b64aa275841b0c9aea3903aba72fbc4275d91 - itsdangerous==2.1.2 - # via flask - jinja2==3.1.3 - # via flask - markupsafe==2.1.5 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 - # via flask + uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979 ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 1 package in [TIME] "### ); Ok(()) } -/// Resolve a specific Flask commit via a Git HTTPS dependency. +/// Resolve a specific tag via a Git HTTPS dependency. #[test] #[cfg(feature = "git")] -fn compile_git_short_commit_https_dependency() -> Result<()> { +fn compile_git_tag_https_dependency() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("flask @ git+https://github.com/pallets/flask.git@d92b64a")?; + requirements_in.write_str( + "uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@test-tag", + )?; uv_snapshot!(context.compile() .arg("requirements.in"), @r###" @@ -1253,36 +1155,107 @@ fn compile_git_short_commit_https_dependency() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - click==8.1.7 - # via flask - flask @ git+https://github.com/pallets/flask.git@d92b64aa275841b0c9aea3903aba72fbc4275d91 - itsdangerous==2.1.2 - # via flask - jinja2==3.1.3 - # via flask - markupsafe==2.1.5 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 - # via flask + uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979 ----- stderr ----- - Resolved 6 packages in [TIME] + Resolved 1 package in [TIME] "### ); Ok(()) } -/// Resolve a specific Flask ref via a Git HTTPS dependency. +/// Resolve a specific tag via a Git HTTPS dependency. +/// +/// In this case, the tag is a date, and thus could feasibly refer to a short commit hash. +#[test] +#[cfg(feature = "git")] +fn compile_git_date_tag_https_dependency() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str( + "uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@20240402", + )?; + + uv_snapshot!(context.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] --exclude-newer 2024-03-25T00:00:00Z requirements.in + uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979 + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} + +/// Resolve a specific commit via a Git HTTPS dependency. +#[test] +#[cfg(feature = "git")] +fn compile_git_long_commit_https_dependency() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str( + "uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979", + )?; + + uv_snapshot!(context.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] --exclude-newer 2024-03-25T00:00:00Z requirements.in + uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979 + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} + +/// Resolve a specific commit via a Git HTTPS dependency. +#[test] +#[cfg(feature = "git")] +fn compile_git_short_commit_https_dependency() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str( + "uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0dacfd6", + )?; + + uv_snapshot!(context.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] --exclude-newer 2024-03-25T00:00:00Z requirements.in + uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@0dacfd662c64cb4ceb16e6cf65a157a8b715b979 + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} + +/// Resolve a specific ref via a Git HTTPS dependency. #[test] #[cfg(feature = "git")] fn compile_git_refs_https_dependency() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in - .write_str("flask @ git+https://github.com/pallets/flask.git@refs/pull/5313/head")?; + .write_str("uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@refs/pull/4/head")?; uv_snapshot!(context.compile() .arg("requirements.in"), @r###" @@ -1291,24 +1264,10 @@ fn compile_git_refs_https_dependency() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in - blinker==1.7.0 - # via flask - click==8.1.7 - # via flask - flask @ git+https://github.com/pallets/flask.git@7af0271f4703a71beef8e26d1f5f6f8da04100e6 - itsdangerous==2.1.2 - # via flask - jinja2==3.1.3 - # via flask - markupsafe==2.1.5 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 - # via flask + uv-public-pypackage @ git+https://github.com/astral-test/uv-public-pypackage@9d01a806f17ddacb9c7b66b1b68574adf790b63f ----- stderr ----- - Resolved 7 packages in [TIME] + Resolved 1 package in [TIME] "### ); From 661787b0cbb542f2e8e3bb4fbcc834a42af59f4b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 Apr 2024 00:33:25 -0400 Subject: [PATCH 006/110] Document that uv is safe to run concurrently (#2818) Closes https://github.com/astral-sh/uv/issues/2730. --- README.md | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a00d547bb..74ba5ef93 100644 --- a/README.md +++ b/README.md @@ -261,8 +261,18 @@ The specifics of uv's caching semantics vary based on the nature of the dependen - **For Git dependencies**, uv caches based on the fully-resolved Git commit hash. As such, `uv pip compile` will pin Git dependencies to a specific commit hash when writing the resolved dependency set. -- **For local dependencies**, uv caches based on the last-modified time of the `setup.py` or - `pyproject.toml` file. +- **For local dependencies**, uv caches based on the last-modified time of the source archive (i.e., + the local `.whl` or `.tar.gz` file). For directories, uv caches based on the last-modified time of + the `pyproject.toml`, `setup.py`, or `setup.cfg` file. + +It's safe to run multiple `uv` commands concurrently, even against the same virtual environment. +uv's cache is designed to be thread-safe and append-only, and thus robust to multiple concurrent +readers and writers. uv applies a file-based lock to the target virtual environment when installing, +to avoid concurrent modifications across processes. + +Note that it's _not_ safe to modify the uv cache directly (e.g., `uv cache clean`) while other `uv` +commands are running, and _never_ safe to modify the cache directly (e.g., by removing a file or +directory). If you're running into caching issues, uv includes a few escape hatches: From ab8368aa27b36a586d6a49ca140b1d54af358d48 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 Apr 2024 13:19:44 -0400 Subject: [PATCH 007/110] Deduplicate editables during install commands (#2820) ## Summary Closes #2819. --- crates/distribution-types/src/editable.rs | 52 +++++++++++++++++++++++ crates/uv-installer/src/downloader.rs | 4 +- crates/uv/src/commands/pip_compile.rs | 13 +++--- crates/uv/src/commands/pip_install.rs | 25 +++++------ crates/uv/src/commands/pip_sync.rs | 36 +++++++--------- crates/uv/tests/pip_compile.rs | 50 ++++++++++++++++++++++ 6 files changed, 136 insertions(+), 44 deletions(-) diff --git a/crates/distribution-types/src/editable.rs b/crates/distribution-types/src/editable.rs index 6b5c397df..5894b1151 100644 --- a/crates/distribution-types/src/editable.rs +++ b/crates/distribution-types/src/editable.rs @@ -1,4 +1,6 @@ use std::borrow::Cow; +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; use std::path::PathBuf; use url::Url; @@ -41,3 +43,53 @@ impl std::fmt::Display for LocalEditable { std::fmt::Display::fmt(&self.url, f) } } + +/// A collection of [`LocalEditable`]s. +#[derive(Debug, Clone)] +pub struct LocalEditables(Vec); + +impl LocalEditables { + /// Merge and dedupe a list of [`LocalEditable`]s. + /// + /// This function will deduplicate any editables that point to identical paths, merging their + /// extras. + pub fn from_editables(editables: impl Iterator) -> Self { + let mut map = BTreeMap::new(); + for editable in editables { + match map.entry(editable.path.clone()) { + Entry::Vacant(entry) => { + entry.insert(editable); + } + Entry::Occupied(mut entry) => { + let existing = entry.get_mut(); + existing.extras.extend(editable.extras); + } + } + } + Self(map.into_values().collect()) + } + + /// Return the number of editables. + pub fn len(&self) -> usize { + self.0.len() + } + + /// Return whether the editables are empty. + pub fn is_empty(&self) -> bool { + self.0.is_empty() + } + + /// Return the editables as a vector. + pub fn into_vec(self) -> Vec { + self.0 + } +} + +impl IntoIterator for LocalEditables { + type Item = LocalEditable; + type IntoIter = std::vec::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} diff --git a/crates/uv-installer/src/downloader.rs b/crates/uv-installer/src/downloader.rs index 2f8f86091..e84405be1 100644 --- a/crates/uv-installer/src/downloader.rs +++ b/crates/uv-installer/src/downloader.rs @@ -9,7 +9,7 @@ use tracing::instrument; use url::Url; use distribution_types::{ - BuildableSource, CachedDist, Dist, Identifier, LocalEditable, RemoteSource, + BuildableSource, CachedDist, Dist, Identifier, LocalEditable, LocalEditables, RemoteSource, }; use platform_tags::Tags; use uv_cache::Cache; @@ -117,7 +117,7 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { #[instrument(skip_all)] pub async fn build_editables( &self, - editables: Vec, + editables: LocalEditables, editable_wheel_dir: &Path, ) -> Result, Error> { // Build editables in parallel diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 31681e883..31f6fdd94 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -14,7 +14,7 @@ use owo_colors::OwoColorize; use tempfile::tempdir_in; use tracing::debug; -use distribution_types::{IndexLocations, LocalEditable, Verbatim}; +use distribution_types::{IndexLocations, LocalEditable, LocalEditables, Verbatim}; use platform_tags::Tags; use requirements_txt::EditableRequirement; use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE}; @@ -300,13 +300,10 @@ pub(crate) async fn pip_compile( } else { let start = std::time::Instant::now(); - let editables: Vec = editables - .into_iter() - .map(|editable| { - let EditableRequirement { url, extras, path } = editable; - Ok(LocalEditable { url, path, extras }) - }) - .collect::>()?; + let editables = LocalEditables::from_editables(editables.into_iter().map(|editable| { + let EditableRequirement { url, extras, path } = editable; + LocalEditable { url, path, extras } + })); let downloader = Downloader::new(&cache, &tags, &client, &build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64)); diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index ba11f3f7a..8e7b398e6 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -10,8 +10,8 @@ use tempfile::tempdir_in; use tracing::debug; use distribution_types::{ - DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, LocalEditable, Name, - Resolution, + DistributionMetadata, IndexLocations, InstalledMetadata, LocalDist, LocalEditable, + LocalEditables, Name, Resolution, }; use install_wheel_rs::linker::LinkMode; use pep508_rs::{MarkerEnvironment, Requirement}; @@ -272,7 +272,7 @@ pub(crate) async fn pip_install( let editables = if editables.is_empty() { vec![] } else { - editable_wheel_dir = tempdir_in(venv.root())?; + editable_wheel_dir = tempdir_in(cache.root())?; build_editables( &editables, editable_wheel_dir.path(), @@ -447,17 +447,14 @@ async fn build_editables( let downloader = Downloader::new(cache, tags, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64)); - let editables: Vec = editables - .iter() - .map(|editable| { - let EditableRequirement { url, extras, path } = editable; - Ok(LocalEditable { - url: url.clone(), - extras: extras.clone(), - path: path.clone(), - }) - }) - .collect::>()?; + let editables = LocalEditables::from_editables(editables.iter().map(|editable| { + let EditableRequirement { url, extras, path } = editable; + LocalEditable { + url: url.clone(), + extras: extras.clone(), + path: path.clone(), + } + })); let editables: Vec<_> = downloader .build_editables(editables, editable_wheel_dir) diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index c5f11d73f..c437de4bc 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -6,7 +6,7 @@ use owo_colors::OwoColorize; use tracing::debug; use distribution_types::{ - IndexLocations, InstalledMetadata, LocalDist, LocalEditable, Name, ResolvedDist, + IndexLocations, InstalledMetadata, LocalDist, LocalEditable, LocalEditables, Name, ResolvedDist, }; use install_wheel_rs::linker::LinkMode; use platform_tags::Tags; @@ -599,32 +599,28 @@ async fn resolve_editables( } else { let start = std::time::Instant::now(); - let temp_dir = tempfile::tempdir_in(cache.root())?; - let downloader = Downloader::new(cache, tags, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(uninstalled.len() as u64)); - let local_editables: Vec = uninstalled - .iter() - .map(|editable| { - let EditableRequirement { url, path, extras } = editable; - Ok(LocalEditable { - url: url.clone(), - path: path.clone(), - extras: extras.clone(), - }) - }) - .collect::>()?; + let editables = LocalEditables::from_editables(uninstalled.iter().map(|editable| { + let EditableRequirement { url, path, extras } = editable; + LocalEditable { + url: url.clone(), + path: path.clone(), + extras: extras.clone(), + } + })); - let built_editables: Vec<_> = downloader - .build_editables(local_editables, temp_dir.path()) + let editable_wheel_dir = tempfile::tempdir_in(cache.root())?; + let editables: Vec<_> = downloader + .build_editables(editables, editable_wheel_dir.path()) .await .context("Failed to build editables")? .into_iter() .collect(); // Validate that the editables are compatible with the target Python version. - for editable in &built_editables { + for editable in &editables { if let Some(python_requires) = editable.metadata.requires_python.as_ref() { if !python_requires.contains(interpreter.python_version()) { return Err(anyhow!( @@ -637,19 +633,19 @@ async fn resolve_editables( } } - let s = if built_editables.len() == 1 { "" } else { "s" }; + let s = if editables.len() == 1 { "" } else { "s" }; writeln!( printer.stderr(), "{}", format!( "Built {} in {}", - format!("{} editable{}", built_editables.len(), s).bold(), + format!("{} editable{}", editables.len(), s).bold(), elapsed(start.elapsed()) ) .dimmed() )?; - (built_editables, Some(temp_dir)) + (editables, Some(editable_wheel_dir)) }; Ok(ResolvedEditables { diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index a1259b62a..c4d199483 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -3096,6 +3096,56 @@ fn compile_editable() -> Result<()> { Ok(()) } +/// If an editable is repeated, it should only be built once. +#[test] +fn deduplicate_editable() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str(indoc! {r" + -e file://../../scripts/packages/black_editable + -e ${PROJECT_ROOT}/../../scripts/packages/black_editable + -e file://../../scripts/packages/black_editable[dev] + " + })?; + + uv_snapshot!(context.filters(), context.compile() + .arg(requirements_in.path()) + .current_dir(current_dir()?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in + -e file://../../scripts/packages/black_editable + aiohttp==3.9.3 + # via black + aiosignal==1.3.1 + # via aiohttp + attrs==23.2.0 + # via aiohttp + frozenlist==1.4.1 + # via + # aiohttp + # aiosignal + idna==3.6 + # via yarl + multidict==6.0.5 + # via + # aiohttp + # yarl + uvloop==0.19.0 + # via black + yarl==1.9.4 + # via aiohttp + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 9 packages in [TIME] + "###); + + Ok(()) +} + #[test] fn recursive_extras_direct_url() -> Result<()> { let context = TestContext::new("3.12"); From 365cb16fd66029d871512f008a02014deb6c0fbf Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 Apr 2024 15:50:52 -0400 Subject: [PATCH 008/110] Bump version to v0.1.29 (#2825) --- CHANGELOG.md | 22 ++++++++++++++++++++++ Cargo.lock | 4 ++-- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- pyproject.toml | 2 +- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d902a336..2457a2133 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,27 @@ # Changelog +## 0.1.29 + +### Enhancements + +- Allow conflicting Git URLs that refer to the same commit SHA ([#2769](https://github.com/astral-sh/uv/pull/2769)) +- Allow package lookups across multiple indexes via explicit opt-in (`--index-strategy unsafe-any-match`) ([#2815](https://github.com/astral-sh/uv/pull/2815)) +- Allow no-op `--no-compile` flag on CLI ([#2816](https://github.com/astral-sh/uv/pull/2816)) +- Upgrade `rs-async-zip` to support data descriptors ([#2809](https://github.com/astral-sh/uv/pull/2809)) + +### Bug fixes + +- Avoid unused extras check in `pip install` for source trees ([#2811](https://github.com/astral-sh/uv/pull/2811)) +- Deduplicate editables during install commands ([#2820](https://github.com/astral-sh/uv/pull/2820)) +- Fix windows lock race: lock exclusive after all try lock errors ([#2800](https://github.com/astral-sh/uv/pull/2800)) +- Preserve `.git` suffixes and casing in Git dependencies ([#2789](https://github.com/astral-sh/uv/pull/2789)) +- Respect Git tags and branches that look like short commits ([#2795](https://github.com/astral-sh/uv/pull/2795)) +- Enable virtualenv creation on Windows with cpython-x86 ([#2707](https://github.com/astral-sh/uv/pull/2707)) + +### Documentation + +- Document that uv is safe to run concurrently ([#2818](https://github.com/astral-sh/uv/pull/2818)) + ## 0.1.28 ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index 7a4676e40..09a8a1be2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4283,7 +4283,7 @@ checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" [[package]] name = "uv" -version = "0.1.28" +version = "0.1.29" dependencies = [ "anstream", "anyhow", @@ -4801,7 +4801,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.1.28" +version = "0.1.29" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 9258fe59b..49f8155d6 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.1.28" +version = "0.1.29" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 2fde36bcf..0dfb749a9 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.1.28" +version = "0.1.29" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/pyproject.toml b/pyproject.toml index e8801643a..35549781d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.1.28" +version = "0.1.29" description = "An extremely fast Python package installer and resolver, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From 2ac562b40d63428d18a0c90c587455d24997f55c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 Apr 2024 22:00:39 -0400 Subject: [PATCH 009/110] Respect `--no-build` and `--no-binary` in `--find-links` (#2826) ## Summary In working on `--require-hashes`, I noticed that we're missing some incompatibility tracking for `--find-links` distributions. Specifically, we don't respect `--no-build` or `--no-binary`, so if we select a wheel due to `--find-links`, we then throw a hard error when trying to build it later (if `--no-binary` is provided), rather than selecting the source distribution instead. Closes https://github.com/astral-sh/uv/issues/2827. --- crates/uv-client/src/flat_index.rs | 90 +++++++++++++----- crates/uv-dev/src/resolve_cli.rs | 21 ++-- crates/uv/src/commands/pip_compile.rs | 2 +- crates/uv/src/commands/pip_install.rs | 2 +- crates/uv/src/commands/pip_sync.rs | 2 +- crates/uv/src/commands/venv.rs | 2 +- crates/uv/tests/pip_compile.rs | 2 +- crates/uv/tests/pip_install.rs | 64 ++++++++++++- crates/uv/tests/pip_sync.rs | 8 +- .../maturin-1.4.0-py3-none-any.whl | Bin .../maturin-2.0.0-py3-none-linux_x86_64.whl | Bin .../simple_launcher-0.1.0-py3-none-any.whl | Bin .../tqdm-1000.0.0-py3-none-any.whl | Bin ...ylinux2010_x86_64.musllinux_1_1_x86_64.whl | Bin scripts/links/tqdm-999.0.0.tar.gz | Bin 0 -> 2127 bytes 15 files changed, 150 insertions(+), 43 deletions(-) rename scripts/{wheels => links}/maturin-1.4.0-py3-none-any.whl (100%) rename scripts/{wheels => links}/maturin-2.0.0-py3-none-linux_x86_64.whl (100%) rename scripts/{wheels => links}/simple_launcher-0.1.0-py3-none-any.whl (100%) rename scripts/{wheels => links}/tqdm-1000.0.0-py3-none-any.whl (100%) rename scripts/{wheels => links}/tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl (100%) create mode 100644 scripts/links/tqdm-999.0.0.tar.gz diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index a042f4f10..45eac5af8 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -8,10 +8,11 @@ use rustc_hash::FxHashMap; use tracing::{debug, info_span, instrument, warn, Instrument}; use url::Url; -use distribution_filename::DistFilename; +use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use distribution_types::{ - BuiltDist, Dist, File, FileLocation, FlatIndexLocation, IndexUrl, PrioritizedDist, - RegistryBuiltDist, RegistrySourceDist, SourceDist, SourceDistCompatibility, + BuiltDist, Dist, File, FileLocation, FlatIndexLocation, IncompatibleSource, IncompatibleWheel, + IndexUrl, PrioritizedDist, RegistryBuiltDist, RegistrySourceDist, SourceDist, + SourceDistCompatibility, WheelCompatibility, }; use pep440_rs::Version; use pep508_rs::VerbatimUrl; @@ -19,6 +20,7 @@ use platform_tags::Tags; use pypi_types::Hashes; use uv_cache::{Cache, CacheBucket}; use uv_normalize::PackageName; +use uv_types::{NoBinary, NoBuild}; use crate::cached_client::{CacheControl, CachedClientError}; use crate::html::SimpleHtml; @@ -271,12 +273,25 @@ pub struct FlatIndex { impl FlatIndex { /// Collect all files from a `--find-links` target into a [`FlatIndex`]. #[instrument(skip_all)] - pub fn from_entries(entries: FlatIndexEntries, tags: &Tags) -> Self { + pub fn from_entries( + entries: FlatIndexEntries, + tags: &Tags, + no_build: &NoBuild, + no_binary: &NoBinary, + ) -> Self { // Collect compatible distributions. let mut index = FxHashMap::default(); for (filename, file, url) in entries.entries { let distributions = index.entry(filename.name().clone()).or_default(); - Self::add_file(distributions, file, filename, tags, url); + Self::add_file( + distributions, + file, + filename, + tags, + no_build, + no_binary, + url, + ); } // Collect offline entries. @@ -290,15 +305,17 @@ impl FlatIndex { file: File, filename: DistFilename, tags: &Tags, + no_build: &NoBuild, + no_binary: &NoBinary, index: IndexUrl, ) { // No `requires-python` here: for source distributions, we don't have that information; // for wheels, we read it lazily only when selected. match filename { DistFilename::WheelFilename(filename) => { - let compatibility = filename.compatibility(tags); let version = filename.version.clone(); + let compatibility = Self::wheel_compatibility(&filename, tags, no_binary); let dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist { filename, file: Box::new(file), @@ -306,20 +323,15 @@ impl FlatIndex { })); match distributions.0.entry(version) { Entry::Occupied(mut entry) => { - entry - .get_mut() - .insert_built(dist, None, compatibility.into()); + entry.get_mut().insert_built(dist, None, compatibility); } Entry::Vacant(entry) => { - entry.insert(PrioritizedDist::from_built( - dist, - None, - compatibility.into(), - )); + entry.insert(PrioritizedDist::from_built(dist, None, compatibility)); } } } DistFilename::SourceDistFilename(filename) => { + let compatibility = Self::source_dist_compatibility(&filename, no_build); let dist = Dist::Source(SourceDist::Registry(RegistrySourceDist { filename: filename.clone(), file: Box::new(file), @@ -327,24 +339,54 @@ impl FlatIndex { })); match distributions.0.entry(filename.version) { Entry::Occupied(mut entry) => { - entry.get_mut().insert_source( - dist, - None, - SourceDistCompatibility::Compatible, - ); + entry.get_mut().insert_source(dist, None, compatibility); } Entry::Vacant(entry) => { - entry.insert(PrioritizedDist::from_source( - dist, - None, - SourceDistCompatibility::Compatible, - )); + entry.insert(PrioritizedDist::from_source(dist, None, compatibility)); } } } } } + fn source_dist_compatibility( + filename: &SourceDistFilename, + no_build: &NoBuild, + ) -> SourceDistCompatibility { + // Check if source distributions are allowed for this package. + let no_build = match no_build { + NoBuild::None => false, + NoBuild::All => true, + NoBuild::Packages(packages) => packages.contains(&filename.name), + }; + + if no_build { + return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild); + } + + SourceDistCompatibility::Compatible + } + + fn wheel_compatibility( + filename: &WheelFilename, + tags: &Tags, + no_binary: &NoBinary, + ) -> WheelCompatibility { + // Check if binaries are allowed for this package. + let no_binary = match no_binary { + NoBinary::None => false, + NoBinary::All => true, + NoBinary::Packages(packages) => packages.contains(&filename.name), + }; + + if no_binary { + return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary); + } + + // Determine a compatibility for the wheel based on tags. + WheelCompatibility::from(filename.compatibility(tags)) + } + /// Get the [`FlatDistributions`] for the given package name. pub fn get(&self, package_name: &PackageName) -> Option<&FlatDistributions> { self.index.get(package_name) diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index bb320628c..3d9bb3cc7 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -56,14 +56,6 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { let venv = PythonEnvironment::from_virtualenv(&cache)?; let index_locations = IndexLocations::new(args.index_url, args.extra_index_url, args.find_links, false); - let client = RegistryClientBuilder::new(cache.clone()) - .index_urls(index_locations.index_urls()) - .build(); - let flat_index = { - let client = FlatIndexClient::new(&client, &cache); - let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, venv.interpreter().tags()?) - }; let index = InMemoryIndex::default(); let in_flight = InFlight::default(); let no_build = if args.no_build { @@ -71,6 +63,19 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { } else { NoBuild::None }; + let client = RegistryClientBuilder::new(cache.clone()) + .index_urls(index_locations.index_urls()) + .build(); + let flat_index = { + let client = FlatIndexClient::new(&client, &cache); + let entries = client.fetch(index_locations.flat_index()).await?; + FlatIndex::from_entries( + entries, + venv.interpreter().tags()?, + &no_build, + &NoBinary::None, + ) + }; let config_settings = ConfigSettings::default(); let build_dispatch = BuildDispatch::new( diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 31f6fdd94..55c781921 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -228,7 +228,7 @@ pub(crate) async fn pip_compile( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, &tags) + FlatIndex::from_entries(entries, &tags, &no_build, &NoBinary::None) }; // Track in-flight downloads, builds, etc., across resolutions. diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 8e7b398e6..80281a2f3 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -206,7 +206,7 @@ pub(crate) async fn pip_install( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, tags) + FlatIndex::from_entries(entries, tags, &no_build, &no_binary) }; // Determine whether to enable build isolation. diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index c437de4bc..28c10c82f 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -155,7 +155,7 @@ pub(crate) async fn pip_sync( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, tags) + FlatIndex::from_entries(entries, tags, &no_build, &no_binary) }; // Create a shared in-memory index. diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index e6c842cbd..160e7e332 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -169,7 +169,7 @@ async fn venv_impl( .fetch(index_locations.flat_index()) .await .map_err(VenvError::FlatIndex)?; - FlatIndex::from_entries(entries, tags) + FlatIndex::from_entries(entries, tags, &NoBuild::All, &NoBinary::None) }; // Create a shared in-memory index. diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index c4d199483..bf26d9ffb 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -3600,7 +3600,7 @@ fn find_links_directory() -> Result<()> { uv_snapshot!(context.filters(), context.compile() .arg("requirements.in") .arg("--find-links") - .arg(context.workspace_root.join("scripts").join("wheels")), @r###" + .arg(context.workspace_root.join("scripts").join("links")), @r###" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 7c2e680f7..acfa3c947 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -1854,7 +1854,7 @@ fn launcher() -> Result<()> { uv_snapshot!( filters, context.install() - .arg(format!("simple_launcher@{}", project_root.join("scripts/wheels/simple_launcher-0.1.0-py3-none-any.whl").display())) + .arg(format!("simple_launcher@{}", project_root.join("scripts/links/simple_launcher-0.1.0-py3-none-any.whl").display())) .arg("--strict"), @r###" success: true exit_code: 0 @@ -1899,7 +1899,7 @@ fn launcher_with_symlink() -> Result<()> { uv_snapshot!(filters, context.install() - .arg(format!("simple_launcher@{}", project_root.join("scripts/wheels/simple_launcher-0.1.0-py3-none-any.whl").display())) + .arg(format!("simple_launcher@{}", project_root.join("scripts/links/simple_launcher-0.1.0-py3-none-any.whl").display())) .arg("--strict"), @r###" success: true @@ -3739,3 +3739,63 @@ fn already_installed_remote_url() { `) "###); } + +/// Sync using `--find-links` with a local directory. +#[test] +fn find_links() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc! {r" + tqdm + "})?; + + uv_snapshot!(context.filters(), context.install() + .arg("tqdm") + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + tqdm==1000.0.0 + "### + ); + + Ok(()) +} + +/// Sync using `--find-links` with a local directory, with wheels disabled. +#[test] +fn find_links_no_binary() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc! {r" + tqdm + "})?; + + uv_snapshot!(context.filters(), context.install() + .arg("tqdm") + .arg("--no-binary") + .arg(":all:") + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + tqdm==999.0.0 + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index b95f001cf..307d425ba 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -2461,7 +2461,7 @@ fn find_links() -> Result<()> { uv_snapshot!(context.filters(), command(&context) .arg("requirements.txt") .arg("--find-links") - .arg(context.workspace_root.join("scripts/wheels/")), @r###" + .arg(context.workspace_root.join("scripts/links/")), @r###" success: true exit_code: 0 ----- stdout ----- @@ -2494,7 +2494,7 @@ fn find_links_no_index_match() -> Result<()> { .arg("requirements.txt") .arg("--no-index") .arg("--find-links") - .arg(context.workspace_root.join("scripts/wheels/")), @r###" + .arg(context.workspace_root.join("scripts/links/")), @r###" success: true exit_code: 0 ----- stdout ----- @@ -2524,7 +2524,7 @@ fn find_links_offline_match() -> Result<()> { .arg("requirements.txt") .arg("--offline") .arg("--find-links") - .arg(context.workspace_root.join("scripts/wheels/")), @r###" + .arg(context.workspace_root.join("scripts/links/")), @r###" success: true exit_code: 0 ----- stdout ----- @@ -2555,7 +2555,7 @@ fn find_links_offline_no_match() -> Result<()> { .arg("requirements.txt") .arg("--offline") .arg("--find-links") - .arg(context.workspace_root.join("scripts/wheels/")), @r###" + .arg(context.workspace_root.join("scripts/links/")), @r###" success: false exit_code: 2 ----- stdout ----- diff --git a/scripts/wheels/maturin-1.4.0-py3-none-any.whl b/scripts/links/maturin-1.4.0-py3-none-any.whl similarity index 100% rename from scripts/wheels/maturin-1.4.0-py3-none-any.whl rename to scripts/links/maturin-1.4.0-py3-none-any.whl diff --git a/scripts/wheels/maturin-2.0.0-py3-none-linux_x86_64.whl b/scripts/links/maturin-2.0.0-py3-none-linux_x86_64.whl similarity index 100% rename from scripts/wheels/maturin-2.0.0-py3-none-linux_x86_64.whl rename to scripts/links/maturin-2.0.0-py3-none-linux_x86_64.whl diff --git a/scripts/wheels/simple_launcher-0.1.0-py3-none-any.whl b/scripts/links/simple_launcher-0.1.0-py3-none-any.whl similarity index 100% rename from scripts/wheels/simple_launcher-0.1.0-py3-none-any.whl rename to scripts/links/simple_launcher-0.1.0-py3-none-any.whl diff --git a/scripts/wheels/tqdm-1000.0.0-py3-none-any.whl b/scripts/links/tqdm-1000.0.0-py3-none-any.whl similarity index 100% rename from scripts/wheels/tqdm-1000.0.0-py3-none-any.whl rename to scripts/links/tqdm-1000.0.0-py3-none-any.whl diff --git a/scripts/wheels/tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl b/scripts/links/tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl similarity index 100% rename from scripts/wheels/tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl rename to scripts/links/tqdm-4.66.1-py3-none-manylinux_2_12_x86_64.manylinux2010_x86_64.musllinux_1_1_x86_64.whl diff --git a/scripts/links/tqdm-999.0.0.tar.gz b/scripts/links/tqdm-999.0.0.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..cbe4150bd7071bd5788a33df0a29aa8031df0758 GIT binary patch literal 2127 zcmV-V2(b4biwFP!5jI`||Lq!UPvc0E&;2Xv%!eIN>^#UYM|6xXBxbNCBvBG(7NICO zb|>-S*Sgz4cD4U~tJ}^CW)`xGqTSmrAvxV$uP#@0Rkh(ixq*Iia$=N>Qjt@}S#dP- zLXVF|Mm*np6DT1ZSF7@g@bpfIAN6z3r;R9{C*E|%2lSEGpi-(-^*Eu7h8!9*JMldVmv+Kt@E<1|8i^5z ziT_Rm+w%=43OdjPr+XZ_(uHW0K?xNN9iu(zfyh{ z|CQRkBC#Qz(o|38xd@rR0kmH!p&H2#gL$GvG7 zCG<^YycGYX(uXJUkLACL|DC|t{uvK?6_hIeG5%Xt!)*6WgFo^e#y^(-PxF7}Sk?c# z0nEsdXKF&06#%?*4*qBXK)N&uJSHl9@EFW!LdSEMCU(fFTYyPIDMS+x9Hz7YJOVqM zLySEE5=3KeWBD0Q!3GDl5tw|2Y*;km4|YNk*#(=i$nk9C2X5q~0V-&7A#Z|Z41;%k zMnUW66xx9l;!@iOF9fl+Sb_(S&#>18+$2vtNAM^hlH;eY&|$Icdx4ir5aiNCOfXFu z?m*~SfFN?ciFi`EtvDU~9-9@w^#tp2%5jp3NonhX*heu+fKlHU96Z!p?scV|#1T?r zVNjl%NXmVfMZxAy&_t6o2~i}KG;V|@lM??&9WG`B`XuuG=s|4Fi9**CJ7P6L2DpSx z|2>s^$vhy8II5bdAYMScdaL;oo7t%QnC1pUxoBQ{Z99n&kKu*oAKE^S5-IQLHVu@2 zVM4!iKKNSinPByy+v|L>nr0KUdLPGH0lr#;i_X;m5TjRb53Zqe4)yjme74%n0+>H` zduG26ogP@1-IitIoYii$u9{Z+6PzJkyMu0Sp|c~?K?j7uoRwwv1+z=D*SNr8{mg1v zgX;pETZ6XXd5%o$(5?3dR^zHw??Lyf*X{I86yHR)ZL59WLn-E^*&Y}u73aYGf&=JZ z)LShns(yvq_oRM}PWQTJeYzOHMW@v?aq`SW&FW_@GZTfIYP9OsWdWM?%lao%Qgx6^ zPom}8eZ4T{6w0gPUt?f(+F~1xPJ7V9VF7p98!Sn`T79zs^`6xi208C_ki9S`QgkE} zq-&cQ7h!7Hcq51+##eoFrAyPSw~$v~5U(Q}s+9Qq=Kr2qZ(f>4;J&Slm+JoyrDys7 zsPtat|K0Te!UG7o`PWT%-nBr7FWK$f@ZF5_nAM6!FPIu^=272eh8Gp%dF&PMcaHfqe4T`No?jEPEkqGr03nBm1AFYtI-?O`-4Swj zc!;ZTV}eP1T?;k$irZFnHo^I8a(@=T3P_xE9)jF8*gTg;y<@+pbE7;+|=C2>ts zEQ_jJF?}l%^r{qaAq`!c$UMj~u`ZM14W_8oE?Mk8%3bEVup%xSGamQ{kWc;D^L8Wt z(W;p_4ITyluHyfwdQ^Ve|9_~e{(mQMGfq9<)!CeJ8VpH7e@Z>9hT#-$w3*GFnUB?? zHY6F^*mmwHR)nXpS~TR6hDS*5JVO%N0TnpHxisR=aqKygWggSW(51{tyjT|1r|`oM zDB5;2vSk|5mip*%c68wC$I~O@-x|sNU(8@oXylI0AELySnh%McB6=bR1mG7~XtdGB zU|mT?-y#;i+t6t6Yk>@j<73kyZ!s54wI;o%eiV!D1Nzt=qzq~`sNy+yM!BsO1Wq@J zu=5E5QF6EJaGKgvDzLJX@HIH%m)P*DQBPivS}tvYTiyb<@>*Ogh<&>1-*Bd}ooSvk zz+5xe#4B@YjJ`@k$D_GdEM3${6Q2!9bEA0_PMsgTQ*8Xwv3UK(G`tI=hfpS%{me%- zVlkFVrY}(xJ&CE2ev~E-eFcRGBpyY+Ap^|F17=8Ko6i<8wegMi2>}i7nc>ojo%;N_ z4TMo0Z1fT|uY%A=Gne5 zZ)3z`T7Dd&jTeW^jV415G#O6fAwoqy%lWgS4ZmS{K94E;w%LPuJpXuFmLEI6X<|vc zGO#nF&YduzAOu~{s|;PD;W&&!oWttDcDRSFdu~v44_H_=m$5LJfp1_)nDoR^d^gQb zdFgb8%yLYe^oboh!qs?^Vtr#LD3tn(aU^ac7PfxoikXo&g$la*{^mGV(Z)qgv=|HV?pwYjb5O<4^p z#t~^_wA8YGW;vfb3F&4jRKM!AYOs+(3-G%9xyd5Sr7zOvnRJsj%c?KZlDaGhby@Az zwz$h)e8V;$#XWdt2k{MiF)u!w*I@0l327|rL^RIt7Kj_DzhCHVftr_#TT`5DO;Ijw zPg35Vq_QQ+cPq+2%|{fLq_=2DGRzvtc7!xn-He{!IOqdR)EGqckr&ai?p*XnLezcB zKl{E#fdT~z6ev)jK!E}U3KS?%pg@5F1qu`>P@q780tE^bC{Un4fr7V$-v9zrv*G|y F0082_7N-CJ literal 0 HcmV?d00001 From 2f386ef440a331af4f160bc807da4cea2bd9b314 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 4 Apr 2024 23:13:01 -0400 Subject: [PATCH 010/110] Show resolution diagnostics after `pip install` (#2829) ## Summary These are shown in `pip compile`, but absent from `pip install`. See: #2828. --- crates/uv/src/commands/pip_install.rs | 11 +++++++++++ crates/uv/tests/pip_install_scenarios.rs | 2 ++ 2 files changed, 13 insertions(+) diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 80281a2f3..af1bb8c32 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -595,6 +595,17 @@ async fn resolve( .dimmed() )?; + // Notify the user of any diagnostics. + for diagnostic in resolution.diagnostics() { + writeln!( + printer.stderr(), + "{}{} {}", + "warning".yellow().bold(), + ":".bold(), + diagnostic.message().bold() + )?; + } + Ok(resolution) } diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index f79a30d19..54d3bb677 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -708,6 +708,7 @@ fn missing_extra() { ----- stderr ----- Resolved 1 package in [TIME] + warning: The package `package-a==1.0.0` does not have an extra named `extra`. Downloaded 1 package in [TIME] Installed 1 package in [TIME] + package-a==1.0.0 @@ -1080,6 +1081,7 @@ fn extra_does_not_exist_backtrack() { ----- stderr ----- Resolved 1 package in [TIME] + warning: The package `package-a==3.0.0` does not have an extra named `extra`. Downloaded 1 package in [TIME] Installed 1 package in [TIME] + package-a==3.0.0 From a0b8d1a99440e7b8a20a9c7704cec70cf64af715 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 10:40:33 -0400 Subject: [PATCH 011/110] Clean up `Error` enum in `Metadata23` (#2835) ## Summary Rename, and remove a bunch of unused variants. --- crates/pypi-types/src/metadata.rs | 110 ++++++++++++---------------- crates/uv-client/src/error.rs | 6 +- crates/uv-distribution/src/error.rs | 6 +- 3 files changed, 56 insertions(+), 66 deletions(-) diff --git a/crates/pypi-types/src/metadata.rs b/crates/pypi-types/src/metadata.rs index f3b267a00..a5653ed91 100644 --- a/crates/pypi-types/src/metadata.rs +++ b/crates/pypi-types/src/metadata.rs @@ -1,9 +1,8 @@ //! Derived from `pypi_types_crate`. -use indexmap::IndexMap; -use std::io; use std::str::FromStr; +use indexmap::IndexMap; use mailparse::{MailHeaderMap, MailParseError}; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -39,38 +38,17 @@ pub struct Metadata23 { /// /// The error type #[derive(Error, Debug)] -pub enum Error { - /// I/O error - #[error(transparent)] - Io(#[from] io::Error), - /// mail parse error +pub enum MetadataError { #[error(transparent)] MailParse(#[from] MailParseError), - /// TOML parse error #[error(transparent)] Toml(#[from] toml::de::Error), - /// Metadata field not found #[error("metadata field {0} not found")] FieldNotFound(&'static str), - /// Unknown distribution type - #[error("unknown distribution type")] - UnknownDistributionType, - /// Metadata file not found - #[error("metadata file not found")] - MetadataNotFound, - /// Invalid project URL (no comma) - #[error("Invalid Project-URL field (missing comma): '{0}'")] - InvalidProjectUrl(String), - /// Multiple metadata files found - #[error("found multiple metadata files: {0:?}")] - MultipleMetadataFiles(Vec), - /// Invalid Version #[error("invalid version: {0}")] Pep440VersionError(VersionParseError), - /// Invalid VersionSpecifier #[error(transparent)] Pep440Error(#[from] VersionSpecifiersParseError), - /// Invalid Requirement #[error(transparent)] Pep508Error(#[from] Pep508Error), #[error(transparent)] @@ -86,20 +64,20 @@ pub enum Error { /// From impl Metadata23 { /// Parse the [`Metadata23`] from a `METADATA` file, as included in a built distribution (wheel). - pub fn parse_metadata(content: &[u8]) -> Result { + pub fn parse_metadata(content: &[u8]) -> Result { let headers = Headers::parse(content)?; let name = PackageName::new( headers .get_first_value("Name") - .ok_or(Error::FieldNotFound("Name"))?, + .ok_or(MetadataError::FieldNotFound("Name"))?, )?; let version = Version::from_str( &headers .get_first_value("Version") - .ok_or(Error::FieldNotFound("Version"))?, + .ok_or(MetadataError::FieldNotFound("Version"))?, ) - .map_err(Error::Pep440VersionError)?; + .map_err(MetadataError::Pep440VersionError)?; let requires_dist = headers .get_all_values("Requires-Dist") .map(|requires_dist| { @@ -135,28 +113,28 @@ impl Metadata23 { /// Read the [`Metadata23`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2 /// or later _and_ none of the required fields (`Requires-Python`, `Requires-Dist`, and /// `Provides-Extra`) are marked as dynamic. - pub fn parse_pkg_info(content: &[u8]) -> Result { + pub fn parse_pkg_info(content: &[u8]) -> Result { let headers = Headers::parse(content)?; // To rely on a source distribution's `PKG-INFO` file, the `Metadata-Version` field must be // present and set to a value of at least `2.2`. let metadata_version = headers .get_first_value("Metadata-Version") - .ok_or(Error::FieldNotFound("Metadata-Version"))?; + .ok_or(MetadataError::FieldNotFound("Metadata-Version"))?; // Parse the version into (major, minor). let (major, minor) = parse_version(&metadata_version)?; if (major, minor) < (2, 2) || (major, minor) >= (3, 0) { - return Err(Error::UnsupportedMetadataVersion(metadata_version)); + return Err(MetadataError::UnsupportedMetadataVersion(metadata_version)); } // If any of the fields we need are marked as dynamic, we can't use the `PKG-INFO` file. let dynamic = headers.get_all_values("Dynamic").collect::>(); for field in dynamic { match field.as_str() { - "Requires-Python" => return Err(Error::DynamicField("Requires-Python")), - "Requires-Dist" => return Err(Error::DynamicField("Requires-Dist")), - "Provides-Extra" => return Err(Error::DynamicField("Provides-Extra")), + "Requires-Python" => return Err(MetadataError::DynamicField("Requires-Python")), + "Requires-Dist" => return Err(MetadataError::DynamicField("Requires-Dist")), + "Provides-Extra" => return Err(MetadataError::DynamicField("Provides-Extra")), _ => (), } } @@ -165,14 +143,14 @@ impl Metadata23 { let name = PackageName::new( headers .get_first_value("Name") - .ok_or(Error::FieldNotFound("Name"))?, + .ok_or(MetadataError::FieldNotFound("Name"))?, )?; let version = Version::from_str( &headers .get_first_value("Version") - .ok_or(Error::FieldNotFound("Version"))?, + .ok_or(MetadataError::FieldNotFound("Version"))?, ) - .map_err(Error::Pep440VersionError)?; + .map_err(MetadataError::Pep440VersionError)?; // The remaining fields are required to be present. let requires_dist = headers @@ -208,29 +186,31 @@ impl Metadata23 { } /// Extract the metadata from a `pyproject.toml` file, as specified in PEP 621. - pub fn parse_pyproject_toml(contents: &str) -> Result { + pub fn parse_pyproject_toml(contents: &str) -> Result { let pyproject_toml: PyProjectToml = toml::from_str(contents)?; let project = pyproject_toml .project - .ok_or(Error::FieldNotFound("project"))?; + .ok_or(MetadataError::FieldNotFound("project"))?; // If any of the fields we need were declared as dynamic, we can't use the `pyproject.toml` file. let dynamic = project.dynamic.unwrap_or_default(); for field in dynamic { match field.as_str() { - "dependencies" => return Err(Error::DynamicField("dependencies")), + "dependencies" => return Err(MetadataError::DynamicField("dependencies")), "optional-dependencies" => { - return Err(Error::DynamicField("optional-dependencies")) + return Err(MetadataError::DynamicField("optional-dependencies")) } - "requires-python" => return Err(Error::DynamicField("requires-python")), - "version" => return Err(Error::DynamicField("version")), + "requires-python" => return Err(MetadataError::DynamicField("requires-python")), + "version" => return Err(MetadataError::DynamicField("version")), _ => (), } } let name = project.name; - let version = project.version.ok_or(Error::FieldNotFound("version"))?; + let version = project + .version + .ok_or(MetadataError::FieldNotFound("version"))?; let requires_python = project.requires_python.map(VersionSpecifiers::from); // Extract the requirements. @@ -309,28 +289,31 @@ pub struct Metadata10 { impl Metadata10 { /// Parse the [`Metadata10`] from a `PKG-INFO` file, as included in a source distribution. - pub fn parse_pkg_info(content: &[u8]) -> Result { + pub fn parse_pkg_info(content: &[u8]) -> Result { let headers = Headers::parse(content)?; let name = PackageName::new( headers .get_first_value("Name") - .ok_or(Error::FieldNotFound("Name"))?, + .ok_or(MetadataError::FieldNotFound("Name"))?, )?; Ok(Self { name }) } } /// Parse a `Metadata-Version` field into a (major, minor) tuple. -fn parse_version(metadata_version: &str) -> Result<(u8, u8), Error> { - let (major, minor) = metadata_version - .split_once('.') - .ok_or(Error::InvalidMetadataVersion(metadata_version.to_string()))?; +fn parse_version(metadata_version: &str) -> Result<(u8, u8), MetadataError> { + let (major, minor) = + metadata_version + .split_once('.') + .ok_or(MetadataError::InvalidMetadataVersion( + metadata_version.to_string(), + ))?; let major = major .parse::() - .map_err(|_| Error::InvalidMetadataVersion(metadata_version.to_string()))?; + .map_err(|_| MetadataError::InvalidMetadataVersion(metadata_version.to_string()))?; let minor = minor .parse::() - .map_err(|_| Error::InvalidMetadataVersion(metadata_version.to_string()))?; + .map_err(|_| MetadataError::InvalidMetadataVersion(metadata_version.to_string()))?; Ok((major, minor)) } @@ -373,7 +356,7 @@ mod tests { use pep440_rs::Version; use uv_normalize::PackageName; - use crate::Error; + use crate::MetadataError; use super::Metadata23; @@ -381,11 +364,11 @@ mod tests { fn test_parse_metadata() { let s = "Metadata-Version: 1.0"; let meta = Metadata23::parse_metadata(s.as_bytes()); - assert!(matches!(meta, Err(Error::FieldNotFound("Name")))); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Name")))); let s = "Metadata-Version: 1.0\nName: asdf"; let meta = Metadata23::parse_metadata(s.as_bytes()); - assert!(matches!(meta, Err(Error::FieldNotFound("Version")))); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); let s = "Metadata-Version: 1.0\nName: asdf\nVersion: 1.0"; let meta = Metadata23::parse_metadata(s.as_bytes()).unwrap(); @@ -404,22 +387,25 @@ mod tests { let s = "Metadata-Version: 1.0\nName: =?utf-8?q?=C3=A4_space?= \nVersion: 1.0"; let meta = Metadata23::parse_metadata(s.as_bytes()); - assert!(matches!(meta, Err(Error::InvalidName(_)))); + assert!(matches!(meta, Err(MetadataError::InvalidName(_)))); } #[test] fn test_parse_pkg_info() { let s = "Metadata-Version: 2.1"; let meta = Metadata23::parse_pkg_info(s.as_bytes()); - assert!(matches!(meta, Err(Error::UnsupportedMetadataVersion(_)))); + assert!(matches!( + meta, + Err(MetadataError::UnsupportedMetadataVersion(_)) + )); let s = "Metadata-Version: 2.2\nName: asdf"; let meta = Metadata23::parse_pkg_info(s.as_bytes()); - assert!(matches!(meta, Err(Error::FieldNotFound("Version")))); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); let s = "Metadata-Version: 2.3\nName: asdf"; let meta = Metadata23::parse_pkg_info(s.as_bytes()); - assert!(matches!(meta, Err(Error::FieldNotFound("Version")))); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("Version")))); let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0"; let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap(); @@ -428,7 +414,7 @@ mod tests { let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nDynamic: Requires-Dist"; let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap_err(); - assert!(matches!(meta, Error::DynamicField("Requires-Dist"))); + assert!(matches!(meta, MetadataError::DynamicField("Requires-Dist"))); let s = "Metadata-Version: 2.3\nName: asdf\nVersion: 1.0\nRequires-Dist: foo"; let meta = Metadata23::parse_pkg_info(s.as_bytes()).unwrap(); @@ -444,7 +430,7 @@ mod tests { name = "asdf" "#; let meta = Metadata23::parse_pyproject_toml(s); - assert!(matches!(meta, Err(Error::FieldNotFound("version")))); + assert!(matches!(meta, Err(MetadataError::FieldNotFound("version")))); let s = r#" [project] @@ -452,7 +438,7 @@ mod tests { dynamic = ["version"] "#; let meta = Metadata23::parse_pyproject_toml(s); - assert!(matches!(meta, Err(Error::DynamicField("version")))); + assert!(matches!(meta, Err(MetadataError::DynamicField("version")))); let s = r#" [project] diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index 38bc66bad..d5f56b1f0 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -146,7 +146,11 @@ pub enum ErrorKind { /// The metadata file could not be parsed. #[error("Couldn't parse metadata of {0} from {1}")] - MetadataParseError(WheelFilename, String, #[source] Box), + MetadataParseError( + WheelFilename, + String, + #[source] Box, + ), /// The metadata file was not found in the wheel. #[error("Metadata file `{0}` was not found in {1}")] diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 95b313b68..fe61b95da 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -48,7 +48,7 @@ pub enum Error { metadata: PackageName, }, #[error("Failed to parse metadata from built wheel")] - Metadata(#[from] pypi_types::Error), + Metadata(#[from] pypi_types::MetadataError), #[error("Failed to read `dist-info` metadata from built wheel")] DistInfo(#[from] install_wheel_rs::Error), #[error("Failed to read zip archive from built wheel")] @@ -62,11 +62,11 @@ pub enum Error { #[error("The source distribution is missing a `PKG-INFO` file")] MissingPkgInfo, #[error("The source distribution does not support static metadata in `PKG-INFO`")] - DynamicPkgInfo(#[source] pypi_types::Error), + DynamicPkgInfo(#[source] pypi_types::MetadataError), #[error("The source distribution is missing a `pyproject.toml` file")] MissingPyprojectToml, #[error("The source distribution does not support static metadata in `pyproject.toml`")] - DynamicPyprojectToml(#[source] pypi_types::Error), + DynamicPyprojectToml(#[source] pypi_types::MetadataError), #[error("Unsupported scheme in URL: {0}")] UnsupportedScheme(String), From 5474c61fedb5ce5072a723fe63aecaf3c83c52e7 Mon Sep 17 00:00:00 2001 From: konsti Date: Fri, 5 Apr 2024 16:51:01 +0200 Subject: [PATCH 012/110] Refactor candidate selector for batch prefetching (#2832) Batch prefetching needs more information from the candidate selector, so i've split `select` into methods. Split out from #2452. No functional changes. --- crates/uv-resolver/src/candidate_selector.rs | 104 ++++++++++++------- crates/uv-resolver/src/version_map.rs | 4 +- 2 files changed, 70 insertions(+), 38 deletions(-) diff --git a/crates/uv-resolver/src/candidate_selector.rs b/crates/uv-resolver/src/candidate_selector.rs index 33a20d73d..31a6783e9 100644 --- a/crates/uv-resolver/src/candidate_selector.rs +++ b/crates/uv-resolver/src/candidate_selector.rs @@ -70,7 +70,31 @@ impl CandidateSelector { pub(crate) fn select<'a, InstalledPackages: InstalledPackagesProvider>( &'a self, package_name: &'a PackageName, - range: &'a Range, + range: &Range, + version_maps: &'a [VersionMap], + preferences: &'a Preferences, + installed_packages: &'a InstalledPackages, + exclusions: &'a Exclusions, + ) -> Option> { + if let Some(preferred) = Self::get_preferred( + package_name, + range, + version_maps, + preferences, + installed_packages, + exclusions, + ) { + return Some(preferred); + } + + self.select_no_preference(package_name, range, version_maps) + } + + /// Get a preferred version if one exists. This is the preference from a lockfile or a locally + /// installed version. + fn get_preferred<'a, InstalledPackages: InstalledPackagesProvider>( + package_name: &'a PackageName, + range: &Range, version_maps: &'a [VersionMap], preferences: &'a Preferences, installed_packages: &'a InstalledPackages, @@ -141,8 +165,12 @@ impl CandidateSelector { } } - // Determine the appropriate prerelease strategy for the current package. - let allow_prerelease = match &self.prerelease_strategy { + None + } + + /// Determine the appropriate prerelease strategy for the current package. + fn allow_prereleases(&self, package_name: &PackageName) -> AllowPreRelease { + match &self.prerelease_strategy { PreReleaseStrategy::Disallow => AllowPreRelease::No, PreReleaseStrategy::Allow => AllowPreRelease::Yes, PreReleaseStrategy::IfNecessary => AllowPreRelease::IfNecessary, @@ -160,46 +188,50 @@ impl CandidateSelector { AllowPreRelease::IfNecessary } } - }; + } + } + /// Select a [`Candidate`] without checking for version preference such as an existing + /// lockfile. + pub(crate) fn select_no_preference<'a>( + &'a self, + package_name: &'a PackageName, + range: &Range, + version_maps: &'a [VersionMap], + ) -> Option { tracing::trace!( - "selecting candidate for package {:?} with range {:?} with {} remote versions", + "selecting candidate for package {} with range {:?} with {} remote versions", package_name, range, version_maps.iter().map(VersionMap::len).sum::(), ); - match &self.resolution_strategy { - ResolutionStrategy::Highest => version_maps.iter().find_map(|version_map| { + let highest = self.use_highest_version(package_name); + let allow_prerelease = self.allow_prereleases(package_name); + + if highest { + version_maps.iter().find_map(|version_map| { Self::select_candidate( version_map.iter().rev(), package_name, range, allow_prerelease, ) - }), - ResolutionStrategy::Lowest => version_maps.iter().find_map(|version_map| { + }) + } else { + version_maps.iter().find_map(|version_map| { Self::select_candidate(version_map.iter(), package_name, range, allow_prerelease) - }), + }) + } + } + + /// By default, we select the latest version, but we also allow using the lowest version instead + /// to check the lower bounds. + pub(crate) fn use_highest_version(&self, package_name: &PackageName) -> bool { + match &self.resolution_strategy { + ResolutionStrategy::Highest => true, + ResolutionStrategy::Lowest => false, ResolutionStrategy::LowestDirect(direct_dependencies) => { - if direct_dependencies.contains(package_name) { - version_maps.iter().find_map(|version_map| { - Self::select_candidate( - version_map.iter(), - package_name, - range, - allow_prerelease, - ) - }) - } else { - version_maps.iter().find_map(|version_map| { - Self::select_candidate( - version_map.iter().rev(), - package_name, - range, - allow_prerelease, - ) - }) - } + !direct_dependencies.contains(package_name) } } } @@ -207,7 +239,7 @@ impl CandidateSelector { /// Select the first-matching [`Candidate`] from a set of candidate versions and files, /// preferring wheels over source distributions. fn select_candidate<'a>( - versions: impl Iterator)>, + versions: impl Iterator)> + ExactSizeIterator, package_name: &'a PackageName, range: &Range, allow_prerelease: AllowPreRelease, @@ -219,10 +251,8 @@ impl CandidateSelector { } let mut prerelease = None; - let mut steps = 0; - for (version, maybe_dist) in versions { - steps += 1; - + let versions_len = versions.len(); + for (step, (version, maybe_dist)) in versions.enumerate() { let candidate = if version.any_prerelease() { if range.contains(version) { match allow_prerelease { @@ -235,7 +265,7 @@ impl CandidateSelector { after {} steps: {:?} version", package_name, range, - steps, + step, version, ); // If pre-releases are allowed, treat them equivalently @@ -276,7 +306,7 @@ impl CandidateSelector { after {} steps: {:?} version", package_name, range, - steps, + step, version, ); Candidate::new(package_name, version, dist) @@ -308,7 +338,7 @@ impl CandidateSelector { after {} steps", package_name, range, - steps, + versions_len, ); match prerelease { None => None, diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index c7937bef8..3d0de4189 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -152,7 +152,9 @@ impl VersionMap { /// which can be used to lazily request a [`CompatibleDist`]. This is /// useful in cases where one can skip materializing a full distribution /// for each version. - pub(crate) fn iter(&self) -> impl DoubleEndedIterator { + pub(crate) fn iter( + &self, + ) -> impl DoubleEndedIterator + ExactSizeIterator { match self.inner { VersionMapInner::Eager(ref map) => { either::Either::Left(map.iter().map(|(version, dist)| { From 37225cb9204dde29580474aba50cb9b0be721ff7 Mon Sep 17 00:00:00 2001 From: Ikko Eltociear Ashimine Date: Sat, 6 Apr 2024 01:53:09 +0900 Subject: [PATCH 013/110] Fix a typo in source_tree.rs (#2836) --- crates/uv-requirements/src/source_tree.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index 580b44979..4b021730a 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -18,7 +18,7 @@ use crate::ExtrasSpecification; /// A resolver for requirements specified via source trees. /// -/// Used, e.g., to determine the the input requirements when a user specifies a `pyproject.toml` +/// Used, e.g., to determine the input requirements when a user specifies a `pyproject.toml` /// file, which may require running PEP 517 build hooks to extract metadata. pub struct SourceTreeResolver<'a, Context: BuildContext + Send + Sync> { /// The requirements for the project. From cedb5e4aec51c61638f63ddce2423e5dfc2a4ec5 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 13:10:23 -0400 Subject: [PATCH 014/110] Add a test for `--offline` direct URLs (#2837) --- crates/uv/tests/pip_compile.rs | 85 ++++++++++++++++++++++++++++++++-- 1 file changed, 81 insertions(+), 4 deletions(-) diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index bf26d9ffb..91252ac7c 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -4319,9 +4319,9 @@ fn matching_index_urls_requirements_txt() -> Result<()> { Ok(()) } -/// Resolve without network access via the `--offline` flag. +/// Resolve a registry package without network access via the `--offline` flag. #[test] -fn offline() -> Result<()> { +fn offline_registry() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("black==23.10.1")?; @@ -4397,8 +4397,8 @@ fn offline() -> Result<()> { Ok(()) } -/// Resolve without network access via the `--offline` flag, using `--find-links` for an HTML -/// registry. +/// Resolve a package without network access via the `--offline` flag, using `--find-links` for an +/// HTML registry. #[test] fn offline_find_links() -> Result<()> { let context = TestContext::new("3.12"); @@ -4449,6 +4449,61 @@ fn offline_find_links() -> Result<()> { Ok(()) } +/// Resolve a direct URL package without network access via the `--offline` flag. +#[test] +fn offline_direct_url() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl")?; + + // Resolve with `--offline` with an empty cache. + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--offline"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download: iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl + Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl` + "### + ); + + // Populate the cache. + uv_snapshot!(context.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] --exclude-newer 2024-03-25T00:00:00Z requirements.in + iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + // Resolve with `--offline` with a populated cache. + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--offline"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --offline + iniconfig @ https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} + /// Resolve nested `-r` requirements files with relative paths. #[test] fn compile_relative_subfile() -> Result<()> { @@ -5847,6 +5902,28 @@ fn no_stream() -> Result<()> { Ok(()) } +/// Resolve a direct URL package with a URL that doesn't exist (i.e., returns a 404). +#[test] +fn not_found_direct_url() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("iniconfig @ https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl")?; + + uv_snapshot!(context.compile() + .arg("requirements.in"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download: iniconfig @ https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl + Caused by: HTTP status client error (404 Not Found) for url (https://files.pythonhosted.org/packages/ef/a6/fake/iniconfig-2.0.0-py3-none-any.whl) + "### + ); + + Ok(()) +} + /// Raise an error when a direct URL dependency's `Requires-Python` constraint is not met. #[test] fn requires_python_direct_url() -> Result<()> { From c11e9e509794be64b69f35b4ae454c47ae63d411 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 14:16:18 -0400 Subject: [PATCH 015/110] Add backtracking tests for distribution incompatibilities (#2839) ## Summary Demonstrates some suboptimal behavior in how we handle invalid metadata, which are fixed in https://github.com/astral-sh/uv/pull/2834. The included wheels were modified by-hand to include invalid structures. --- crates/uv/tests/pip_compile.rs | 128 ++++++++++++++++++ .../links/validation-1.0.0-py3-none-any.whl | Bin 0 -> 1410 bytes .../links/validation-2.0.0-py3-none-any.whl | Bin 0 -> 1405 bytes .../links/validation-3.0.0-py3-none-any.whl | Bin 0 -> 2446 bytes 4 files changed, 128 insertions(+) create mode 100644 scripts/links/validation-1.0.0-py3-none-any.whl create mode 100644 scripts/links/validation-2.0.0-py3-none-any.whl create mode 100644 scripts/links/validation-3.0.0-py3-none-any.whl diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 91252ac7c..5f50d655c 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -4397,6 +4397,50 @@ fn offline_registry() -> Result<()> { Ok(()) } +/// Resolve a registry package without network access via the `--offline` flag. We should backtrack +/// to the latest version of the package that's available in the cache. +#[test] +fn offline_registry_backtrack() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("iniconfig==1.1.1")?; + + // Populate the cache. + uv_snapshot!(context.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] --exclude-newer 2024-03-25T00:00:00Z requirements.in + iniconfig==1.1.1 + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + // Resolve with `--offline`, with a looser requirement. We should backtrack to `1.1.1`, but + // we don't right now. + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("iniconfig")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--offline"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download: iniconfig==2.0.0 + Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl.metadata` + "### + ); + + Ok(()) +} + /// Resolve a package without network access via the `--offline` flag, using `--find-links` for an /// HTML registry. #[test] @@ -4504,6 +4548,90 @@ fn offline_direct_url() -> Result<()> { Ok(()) } +/// Resolve a package with invalid metadata, by way of an invalid `Requires-Python` field in the +/// `METADATA` file. +#[test] +fn invalid_metadata_requires_python() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("validation==2.0.0")?; + + // `2.0.0` has invalid metadata. + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--no-index") + .arg("--find-links") + .arg(context.workspace_root.join("scripts").join("links")), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download: validation==2.0.0 + Caused by: Couldn't parse metadata of validation-2.0.0-py3-none-any.whl from validation==2.0.0 + Caused by: Failed to parse version: Unexpected end of version specifier, expected operator: + 12 + ^^ + + "### + ); + + Ok(()) +} + +/// Resolve a package with multiple `.dist-info` directories. +#[test] +fn invalid_metadata_multiple_dist_info() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("validation==3.0.0")?; + + // `3.0.0` has an invalid structure (multiple `.dist-info` directories). + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--no-index") + .arg("--find-links") + .arg(context.workspace_root.join("scripts").join("links")), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download: validation==3.0.0 + Caused by: Multiple .dist-info directories found: validation-2.0.0, validation-3.0.0 + "### + ); + + Ok(()) +} + +/// Resolve a package, but backtrack past versions with invalid metadata. +#[test] +fn invalid_metadata_backtrack() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("validation")?; + + // `2.0.0` and `3.0.0` have invalid metadata. We should backtrack to `1.0.0` (the preceding + // version, which has valid metadata), but we don't right now. + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--no-index") + .arg("--find-links") + .arg(context.workspace_root.join("scripts").join("links")), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download: validation==3.0.0 + Caused by: Multiple .dist-info directories found: validation-2.0.0, validation-3.0.0 + "### + ); + + Ok(()) +} + /// Resolve nested `-r` requirements files with relative paths. #[test] fn compile_relative_subfile() -> Result<()> { diff --git a/scripts/links/validation-1.0.0-py3-none-any.whl b/scripts/links/validation-1.0.0-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..8808be42fc19d35c1659183a7e41f3a1f45e17a1 GIT binary patch literal 1410 zcmWIWW@h1H0DV3IeF&%<^KNjXHM&M_FvataYgf-hl!Et z=C7B|UeYx^p`%$)%58XB$Ll2T`Lk;{bb7w{`g}ZX*t5ZeXC=qkv**8j{P6XPq0u9) zX6a?wE7BQSG-n=F-8Hi^^XIR9>lFL+`V{-DtbT;FR`1yL>p{5ClUHx#pD=|mgQEoE zFHn>`*q>G#2J}-n5G&yKSGb3(s}H`&;XP@{*Py_|aA5EH2h6IAq~#6rj!DeDXyV`$ z6qU|)%bfRwVdAs z_E!FC(eaVkahM$xxe)it0^^~-qWQBk(5=2etc>5izOEsTE{-9NU?)dnkBQH|x}Lz8 z@Vvt7t*dqJ%=yhh2A7PVeDXfyyERAy7)vat7+rTh*wj|VR>cTxLI5Q&`dP*;^WIw zi;97^#>XqzDkvH18R!`(aUpC$cd9gsQ+16%>h)4Gi%WDf^V0GWPPfADbdVP~7_I^R z&%Sg`b}f*%3y2l*8ye*5>>uO;_QhkM51wH9LfiYyNgq$0lV{Ff?(aW;=Cn>{|8?yZ zS2WLgm>8LE{(9-`C0)}KI+_Kg+=i!hyiW3-Kf8uQr{{~W&&Sh-JsV7TR&ty@d;ZJE z4_~hs8a>i#mR^>PJXx^^RS?9)t@$dG$vA z2~!9&I7%S?0!7J#{b{vfKtF{8u>yX7g?qTV`rwNk-jjxW4GKIA2llRiz^uARTHYY< zn8e(RCJs(f!37V#SG}Iwx#EFi)aDK5=lBeSC%37+^5uH#r&|0cx#ZeGwMzvnckJ_0 zjlXA7%lS=UZ{@ER9UqAuhuJ}q3vq8oerv?zB|^P+K(~4Uu`+)5`nrZVx;Tb7f}Q*Y z7{T8#Bl)wht|u@iJg@M2>uQ}lbAEG>!6oA-pS;ibZVl1^#uCdZM%SGaQh2}c>}U$& z_BQSg>Mmv0@-}if)aDig3`#~OIc8j$L;~nU5D;K^>jTxLI7IErxmV@nxw+ z#Xwu*;}vWblnnI@^bC}^5VoK@RT{;qx<(-NdMTO3CAyh;Y554JTVZ!P$O{|{*MR_nQs1i>Fgz4(-S(H1*P1Er**te@}57thC`?4i?7ee(}q18On6puoIQK~%f}C2 zuNWFV(rT7omc1mMp+$4%QPo{DD>Hxo+P6-zPp?n0&&ujYNNe?uUB4cL3q5)DM*ay? z2s1cJApQbH$%FlAwP8R%g#)nyet(5~xVrk_iyYpQhI|bQJPZf+u7AL+x=32yAn%yO z+>0g-PEo-H558Bup4++Nfn(I>4d&$dh4fJ{3p5O+CjBT1uJ*#^HPn! zXHv`gO<-^3uNEC2i5-X8L6Hk_Z$^G=#N#DGy>>vidI7OAe)syihB&%7hB$(q`~?`n zSYqO{udXLBCOoh3dh2SPJ9B<>kijM6C!f5}_-+l-0LBu_DMr_w6H<7;@a$*`;`TP~ z4(cvt*77!TIMn7AgOs`nq&s6=>8=Aai6i`brl2+AqMl(Mj&z4^C{ntc4>WcmrqR?- zcWY>n?$G^(n(n$Vy+!MEcLEp>B~1KT4%pKjx_eR5-E^RHXJR^;sC4&W)3GYHDz+HM zm90x>x~@FBi*c%|q(Y*zvK}zyF*3<9<0_sdfDQ!#0fx7ZAR48xW`z{i7zH)LK+K{V zVj!?kXIRqcifSNA!3{JURB+=l8#ALIJ9agS*;tBee2&LeSR*^00q6%rXkb_X%R)eF zKt(rJf8Z)Rk*&Fb-x_F&#ur++N<3s+I+*Z!29XYd7J;vw*V literal 0 HcmV?d00001 From f0b0e1943cdedf70390e3606979657c0f97c8f23 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Fri, 5 Apr 2024 16:50:56 -0500 Subject: [PATCH 016/110] Update `fetch-version-metadata` to only require the stdlib (#2838) --- scripts/bootstrap/fetch-version-metadata.py | 52 ++++++--------------- 1 file changed, 14 insertions(+), 38 deletions(-) diff --git a/scripts/bootstrap/fetch-version-metadata.py b/scripts/bootstrap/fetch-version-metadata.py index d3d5e4b25..389269f44 100755 --- a/scripts/bootstrap/fetch-version-metadata.py +++ b/scripts/bootstrap/fetch-version-metadata.py @@ -4,13 +4,9 @@ Fetch Python version metadata. Generates the bootstrap `versions.json` file. -Installation: - - pip install requests==2.31.0 - Usage: - scripts/bootstrap/fetch-versions + python fetch-version-metadata.py Acknowledgements: @@ -22,19 +18,13 @@ import argparse import hashlib import json import logging -import os import re -import sys +import urllib.error +import urllib.request from itertools import chain from pathlib import Path from urllib.parse import unquote -try: - import requests -except ImportError: - print("ERROR: requests is required; install with `pip install requests==2.31.0`") - sys.exit(1) - SELF_DIR = Path(__file__).parent RELEASE_URL = "https://api.github.com/repos/indygreg/python-build-standalone/releases" HEADERS = { @@ -129,11 +119,13 @@ def normalize_triple(triple): return "%s-%s-%s" % (arch, platform, libc) -def read_sha256(session, url): - resp = session.get(url + ".sha256") - if not resp.ok: +def read_sha256(url): + try: + resp = urllib.request.urlopen(url + ".sha256") + except urllib.error.HTTPError: return None - return resp.text.strip() + assert resp.status == 200 + return resp.read().strip() def sha256(path): @@ -159,32 +151,16 @@ def _sort_key(info): return pref -def get_session() -> requests.Session: - session = requests.Session() - session.headers = HEADERS.copy() - - token = os.environ.get("GITHUB_TOKEN") - if token: - session.headers["Authorization"] = "Bearer " + token - else: - logging.warning( - "An authentication token was not found at `GITHUB_TOKEN`, rate limits may be encountered.", - ) - - return session - - -def find(args): +def find(): """ Find available Python versions and write metadata to a file. """ results = {} - session = get_session() for page in range(1, 100): logging.debug("Reading release page %s...", page) - resp = session.get("%s?page=%d" % (RELEASE_URL, page)) - rows = resp.json() + resp = urllib.request.urlopen("%s?page=%d" % (RELEASE_URL, page)) + rows = json.loads(resp.read()) if not rows: break for row in rows: @@ -226,7 +202,7 @@ def find(args): for (arch, platform, libc), url in sorted(choices.items()): key = "%s-%s.%s.%s-%s-%s-%s" % (interpreter, *py_ver, platform, arch, libc) logging.info("Found %s", key) - sha256 = read_sha256(session, url) + sha256 = read_sha256(url) final_results[key] = { "name": interpreter, @@ -273,7 +249,7 @@ def main(): datefmt="%Y-%m-%d %H:%M:%S", ) - find(args) + find() if __name__ == "__main__": From 00934044aaa6042954d58dbd72e1311328459e13 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 18:00:48 -0400 Subject: [PATCH 017/110] Backtrack on distributions with invalid metadata (#2834) ## Summary Closes https://github.com/astral-sh/uv/issues/2821. --- Cargo.lock | 1 + crates/uv-client/src/error.rs | 2 +- crates/uv-client/src/registry_client.rs | 2 +- crates/uv-client/src/remote_metadata.rs | 2 +- crates/uv-requirements/src/lookahead.rs | 18 +++- crates/uv-requirements/src/source_tree.rs | 22 +++-- crates/uv-requirements/src/unnamed.rs | 12 ++- crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/lib.rs | 2 +- crates/uv-resolver/src/resolution.rs | 32 +++++-- crates/uv-resolver/src/resolver/index.rs | 17 ++-- crates/uv-resolver/src/resolver/mod.rs | 97 ++++++++++++++++++--- crates/uv-resolver/src/resolver/provider.rs | 44 ++++++++-- crates/uv/tests/pip_compile.rs | 47 +++++----- crates/uv/tests/pip_sync.rs | 3 +- 15 files changed, 229 insertions(+), 73 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 09a8a1be2..985298c32 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4754,6 +4754,7 @@ dependencies = [ "futures", "indexmap", "insta", + "install-wheel-rs", "itertools 0.12.1", "once-map", "once_cell", diff --git a/crates/uv-client/src/error.rs b/crates/uv-client/src/error.rs index d5f56b1f0..af69e27a0 100644 --- a/crates/uv-client/src/error.rs +++ b/crates/uv-client/src/error.rs @@ -132,7 +132,7 @@ pub enum ErrorKind { /// Dist-info error #[error(transparent)] - InstallWheel(#[from] install_wheel_rs::Error), + DistInfo(#[from] install_wheel_rs::Error), #[error("{0} isn't available locally, but making network requests to registries was banned.")] NoIndex(String), diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index b5f51d76e..cf2b23771 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -632,7 +632,7 @@ async fn read_metadata_async_seek( .enumerate() .filter_map(|(index, entry)| Some((index, entry.filename().as_str().ok()?))), ) - .map_err(ErrorKind::InstallWheel)?; + .map_err(ErrorKind::DistInfo)?; // Read the contents of the `METADATA` file. let mut contents = Vec::new(); diff --git a/crates/uv-client/src/remote_metadata.rs b/crates/uv-client/src/remote_metadata.rs index 11b156c70..954212588 100644 --- a/crates/uv-client/src/remote_metadata.rs +++ b/crates/uv-client/src/remote_metadata.rs @@ -75,7 +75,7 @@ pub(crate) async fn wheel_metadata_from_remote_zip( .enumerate() .filter_map(|(idx, e)| Some(((idx, e), e.filename().as_str().ok()?))), ) - .map_err(ErrorKind::InstallWheel)?; + .map_err(ErrorKind::DistInfo)?; let offset = metadata_entry.header_offset(); let size = metadata_entry.compressed_size() diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index 775603968..685b475ab 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -11,7 +11,7 @@ use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use pypi_types::Metadata23; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, Reporter}; -use uv_resolver::InMemoryIndex; +use uv_resolver::{InMemoryIndex, MetadataResponse}; use uv_types::{BuildContext, Constraints, Overrides, RequestedRequirements}; /// A resolver for resolving lookahead requirements from direct URLs. @@ -134,7 +134,18 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { // Fetch the metadata for the distribution. let requires_dist = { let id = dist.package_id(); - if let Some(metadata) = self.index.get_metadata(&id) { + if let Some(metadata) = self + .index + .get_metadata(&id) + .as_deref() + .and_then(|response| { + if let MetadataResponse::Found(metadata) = response { + Some(metadata) + } else { + None + } + }) + { // If the metadata is already in the index, return it. metadata.requires_dist.clone() } else { @@ -151,7 +162,8 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { let requires_dist = metadata.requires_dist.clone(); // Insert the metadata into the index. - self.index.insert_metadata(id, metadata); + self.index + .insert_metadata(id, MetadataResponse::Found(metadata)); requires_dist } diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index 4b021730a..a734db17b 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -1,5 +1,5 @@ use std::borrow::Cow; -use std::ops::Deref; + use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; @@ -11,7 +11,7 @@ use pep508_rs::Requirement; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, Reporter}; use uv_fs::Simplified; -use uv_resolver::InMemoryIndex; +use uv_resolver::{InMemoryIndex, MetadataResponse}; use uv_types::BuildContext; use crate::ExtrasSpecification; @@ -87,16 +87,28 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { // Fetch the metadata for the distribution. let metadata = { let id = PackageId::from_url(source.url()); - if let Some(metadata) = self.index.get_metadata(&id) { + if let Some(metadata) = self + .index + .get_metadata(&id) + .as_deref() + .and_then(|response| { + if let MetadataResponse::Found(metadata) = response { + Some(metadata) + } else { + None + } + }) + { // If the metadata is already in the index, return it. - metadata.deref().clone() + metadata.clone() } else { // Run the PEP 517 build process to extract metadata from the source distribution. let source = BuildableSource::Url(source); let metadata = self.database.build_wheel_metadata(&source).await?; // Insert the metadata into the index. - self.index.insert_metadata(id, metadata.clone()); + self.index + .insert_metadata(id, MetadataResponse::Found(metadata.clone())); metadata } diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index 905594686..96e5cc179 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -20,7 +20,7 @@ use pypi_types::Metadata10; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, Reporter}; use uv_normalize::PackageName; -use uv_resolver::InMemoryIndex; +use uv_resolver::{InMemoryIndex, MetadataResponse}; use uv_types::BuildContext; /// Like [`RequirementsSpecification`], but with concrete names for all requirements. @@ -236,7 +236,13 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont // Fetch the metadata for the distribution. let name = { let id = PackageId::from_url(source.url()); - if let Some(metadata) = index.get_metadata(&id) { + if let Some(metadata) = index.get_metadata(&id).as_deref().and_then(|response| { + if let MetadataResponse::Found(metadata) = response { + Some(metadata) + } else { + None + } + }) { // If the metadata is already in the index, return it. metadata.name.clone() } else { @@ -247,7 +253,7 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont let name = metadata.name.clone(); // Insert the metadata into the index. - index.insert_metadata(id, metadata); + index.insert_metadata(id, MetadataResponse::Found(metadata)); name } diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index de2d97827..ece532d2b 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -16,6 +16,7 @@ workspace = true cache-key = { workspace = true } distribution-filename = { workspace = true, features = ["serde"] } distribution-types = { workspace = true } +install-wheel-rs = { workspace = true } once-map = { workspace = true } pep440_rs = { workspace = true } pep508_rs = { workspace = true } diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 129bbac3e..9c083ee3f 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -9,7 +9,7 @@ pub use python_requirement::PythonRequirement; pub use resolution::{AnnotationStyle, Diagnostic, DisplayResolutionGraph, ResolutionGraph}; pub use resolution_mode::ResolutionMode; pub use resolver::{ - BuildId, DefaultResolverProvider, InMemoryIndex, PackageVersionsResult, + BuildId, DefaultResolverProvider, InMemoryIndex, MetadataResponse, PackageVersionsResult, Reporter as ResolverReporter, Resolver, ResolverProvider, VersionsResponse, WheelMetadataResult, }; diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index b1c29d34f..c3e0dcb84 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -18,7 +18,7 @@ use distribution_types::{ use once_map::OnceMap; use pep440_rs::Version; use pep508_rs::MarkerEnvironment; -use pypi_types::{Hashes, Metadata23}; +use pypi_types::Hashes; use uv_distribution::to_precise; use uv_normalize::{ExtraName, PackageName}; @@ -28,7 +28,7 @@ use crate::pins::FilePins; use crate::preferences::Preferences; use crate::pubgrub::{PubGrubDistribution, PubGrubPackage}; use crate::redirect::apply_redirect; -use crate::resolver::{InMemoryIndex, VersionsResponse}; +use crate::resolver::{InMemoryIndex, MetadataResponse, VersionsResponse}; use crate::{Manifest, ResolveError}; /// Indicate the style of annotation comments, used to indicate the dependencies that requested each @@ -66,7 +66,7 @@ impl ResolutionGraph { selection: &SelectedDependencies, pins: &FilePins, packages: &OnceMap, - distributions: &OnceMap, + distributions: &OnceMap, state: &State, preferences: &Preferences, editables: Editables, @@ -164,13 +164,20 @@ impl ResolutionGraph { }); } } else { - let metadata = distributions.get(&dist.package_id()).unwrap_or_else(|| { + let response = distributions.get(&dist.package_id()).unwrap_or_else(|| { panic!( "Every package should have metadata: {:?}", dist.package_id() ) }); + let MetadataResponse::Found(metadata) = &*response else { + panic!( + "Every package should have metadata: {:?}", + dist.package_id() + ) + }; + if metadata.provides_extras.contains(extra) { extras .entry(package_name.clone()) @@ -211,13 +218,20 @@ impl ResolutionGraph { }); } } else { - let metadata = distributions.get(&dist.package_id()).unwrap_or_else(|| { + let response = distributions.get(&dist.package_id()).unwrap_or_else(|| { panic!( "Every package should have metadata: {:?}", dist.package_id() ) }); + let MetadataResponse::Found(metadata) = &*response else { + panic!( + "Every package should have metadata: {:?}", + dist.package_id() + ) + }; + if metadata.provides_extras.contains(extra) { extras .entry(package_name.clone()) @@ -417,10 +431,16 @@ impl ResolutionGraph { } VersionOrUrl::Url(verbatim_url) => PackageId::from_url(verbatim_url.raw()), }; - let md = index + let res = index .distributions .get(&package_id) .expect("every package in resolution graph has metadata"); + let MetadataResponse::Found(md) = &*res else { + panic!( + "Every package should have metadata: {:?}", + dist.package_id() + ) + }; for req in manifest.apply(&md.requires_dist) { let Some(ref marker_tree) = req.marker else { continue; diff --git a/crates/uv-resolver/src/resolver/index.rs b/crates/uv-resolver/src/resolver/index.rs index 7b4e1e336..af42c9a6f 100644 --- a/crates/uv-resolver/src/resolver/index.rs +++ b/crates/uv-resolver/src/resolver/index.rs @@ -2,10 +2,9 @@ use std::sync::Arc; use distribution_types::PackageId; use once_map::OnceMap; -use pypi_types::Metadata23; use uv_normalize::PackageName; -use crate::resolver::provider::VersionsResponse; +use crate::resolver::provider::{MetadataResponse, VersionsResponse}; /// In-memory index of package metadata. #[derive(Default)] @@ -15,18 +14,18 @@ pub struct InMemoryIndex { pub(crate) packages: OnceMap, /// A map from package ID to metadata for that distribution. - pub(crate) distributions: OnceMap, + pub(crate) distributions: OnceMap, } impl InMemoryIndex { /// Insert a [`VersionsResponse`] into the index. - pub fn insert_package(&self, package_name: PackageName, metadata: VersionsResponse) { - self.packages.done(package_name, metadata); + pub fn insert_package(&self, package_name: PackageName, response: VersionsResponse) { + self.packages.done(package_name, response); } /// Insert a [`Metadata23`] into the index. - pub fn insert_metadata(&self, package_id: PackageId, metadata: Metadata23) { - self.distributions.done(package_id, metadata); + pub fn insert_metadata(&self, package_id: PackageId, response: MetadataResponse) { + self.distributions.done(package_id, response); } /// Get the [`VersionsResponse`] for a given package name, without waiting. @@ -34,8 +33,8 @@ impl InMemoryIndex { self.packages.get(package_name) } - /// Get the [`Metadata23`] for a given package ID, without waiting. - pub fn get_metadata(&self, package_id: &PackageId) -> Option> { + /// Get the [`MetadataResponse`] for a given package ID, without waiting. + pub fn get_metadata(&self, package_id: &PackageId) -> Option> { self.distributions.get(package_id) } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 379a18c0c..b90748b1e 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -14,7 +14,7 @@ use pubgrub::range::Range; use pubgrub::solver::{Incompatibility, State}; use rustc_hash::{FxHashMap, FxHashSet}; use tokio_stream::wrappers::ReceiverStream; -use tracing::{debug, info_span, instrument, trace, Instrument}; +use tracing::{debug, info_span, instrument, trace, warn, Instrument}; use distribution_types::{ BuiltDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource, IncompatibleWheel, @@ -46,8 +46,8 @@ use crate::python_requirement::PythonRequirement; use crate::resolution::ResolutionGraph; pub use crate::resolver::index::InMemoryIndex; pub use crate::resolver::provider::{ - DefaultResolverProvider, PackageVersionsResult, ResolverProvider, VersionsResponse, - WheelMetadataResult, + DefaultResolverProvider, MetadataResponse, PackageVersionsResult, ResolverProvider, + VersionsResponse, WheelMetadataResult, }; use crate::resolver::reporter::Facade; pub use crate::resolver::reporter::{BuildId, Reporter}; @@ -77,6 +77,10 @@ pub(crate) enum UnavailablePackage { Offline, /// The package was not found in the registry NotFound, + /// The wheel metadata was found, but could not be parsed. + InvalidMetadata, + /// The wheel has an invalid structure. + InvalidStructure, } enum ResolverVersion { @@ -342,6 +346,12 @@ impl< UnavailablePackage::NotFound => { "was not found in the package registry" } + UnavailablePackage::InvalidMetadata => { + "was found, but the metadata could not be parsed" + } + UnavailablePackage::InvalidStructure => { + "was found, but has an invalid format" + } }) } else { None @@ -590,12 +600,33 @@ impl< } let dist = PubGrubDistribution::from_url(package_name, url); - let metadata = self + let response = self .index .distributions .wait(&dist.package_id()) .await .ok_or(ResolveError::Unregistered)?; + + // If we failed to fetch the metadata for a URL, we can't proceed. + let metadata = match &*response { + MetadataResponse::Found(metadata) => metadata, + MetadataResponse::Offline => { + self.unavailable_packages + .insert(package_name.clone(), UnavailablePackage::Offline); + return Ok(None); + } + MetadataResponse::InvalidMetadata(_) => { + self.unavailable_packages + .insert(package_name.clone(), UnavailablePackage::InvalidMetadata); + return Ok(None); + } + MetadataResponse::InvalidStructure(_) => { + self.unavailable_packages + .insert(package_name.clone(), UnavailablePackage::InvalidStructure); + return Ok(None); + } + }; + let version = &metadata.version; // The version is incompatible with the requirement. @@ -711,7 +742,6 @@ impl< let version = candidate.version().clone(); // Emit a request to fetch the metadata for this version. - if self.index.distributions.register(candidate.package_id()) { let request = match dist.for_resolution() { ResolvedDistRef::Installable(dist) => Request::Dist(dist.clone()), @@ -866,7 +896,7 @@ impl< } // Wait for the metadata to be available. - let metadata = self + let response = self .index .distributions .wait(&package_id) @@ -874,6 +904,26 @@ impl< .await .ok_or(ResolveError::Unregistered)?; + let metadata = match *response { + MetadataResponse::Found(ref metadata) => metadata, + MetadataResponse::Offline => { + return Ok(Dependencies::Unavailable( + "network connectivity is disabled, but the metadata wasn't found in the cache" + .to_string(), + )); + } + MetadataResponse::InvalidMetadata(_) => { + return Ok(Dependencies::Unavailable( + "the package metadata could not be parsed".to_string(), + )); + } + MetadataResponse::InvalidStructure(_) => { + return Ok(Dependencies::Unavailable( + "the package has an invalid format".to_string(), + )); + } + }; + let mut constraints = PubGrubDependencies::from_requirements( &metadata.requires_dist, &self.constraints, @@ -923,23 +973,41 @@ impl< } Some(Response::Installed { dist, metadata }) => { trace!("Received installed distribution metadata for: {dist}"); - self.index.distributions.done(dist.package_id(), metadata); + self.index + .distributions + .done(dist.package_id(), MetadataResponse::Found(metadata)); } Some(Response::Dist { dist: Dist::Built(dist), metadata, }) => { trace!("Received built distribution metadata for: {dist}"); + match &metadata { + MetadataResponse::InvalidMetadata(err) => { + warn!("Unable to extract metadata for {dist}: {err}"); + } + MetadataResponse::InvalidStructure(err) => { + warn!("Unable to extract metadata for {dist}: {err}"); + } + _ => {} + } self.index.distributions.done(dist.package_id(), metadata); } Some(Response::Dist { - dist: Dist::Source(distribution), + dist: Dist::Source(dist), metadata, }) => { - trace!("Received source distribution metadata for: {distribution}"); - self.index - .distributions - .done(distribution.package_id(), metadata); + trace!("Received source distribution metadata for: {dist}"); + match &metadata { + MetadataResponse::InvalidMetadata(err) => { + warn!("Unable to extract metadata for {dist}: {err}"); + } + MetadataResponse::InvalidStructure(err) => { + warn!("Unable to extract metadata for {dist}: {err}"); + } + _ => {} + } + self.index.distributions.done(dist.package_id(), metadata); } None => {} } @@ -1147,7 +1215,10 @@ enum Response { /// The returned metadata for a package hosted on a registry. Package(PackageName, VersionsResponse), /// The returned metadata for a distribution. - Dist { dist: Dist, metadata: Metadata23 }, + Dist { + dist: Dist, + metadata: MetadataResponse, + }, /// The returned metadata for an already-installed distribution. Installed { dist: InstalledDist, diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 273fc2b53..af89f5820 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -16,7 +16,7 @@ use crate::version_map::VersionMap; use crate::yanks::AllowedYanks; pub type PackageVersionsResult = Result; -pub type WheelMetadataResult = Result; +pub type WheelMetadataResult = Result; /// The response when requesting versions for a package #[derive(Debug)] @@ -31,6 +31,18 @@ pub enum VersionsResponse { Offline, } +#[derive(Debug)] +pub enum MetadataResponse { + /// The wheel metadata was found and parsed successfully. + Found(Metadata23), + /// The wheel metadata was found, but could not be parsed. + InvalidMetadata(Box), + /// The wheel has an invalid structure. + InvalidStructure(Box), + /// The wheel metadata was not found in the cache and the network is not available. + Offline, +} + pub trait ResolverProvider: Send + Sync { /// Get the version map for a package. fn get_package_versions<'io>( @@ -108,11 +120,7 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider &'io self, package_name: &'io PackageName, ) -> PackageVersionsResult { - let result = self.client.simple(package_name).await; - - // If the "Simple API" request was successful, convert to `VersionMap` on the Tokio - // threadpool, since it can be slow. - match result { + match self.client.simple(package_name).await { Ok(results) => Ok(VersionsResponse::Found( results .into_iter() @@ -161,8 +169,30 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider } } + /// Fetch the metadata for a distribution, building it if necessary. async fn get_or_build_wheel_metadata<'io>(&'io self, dist: &'io Dist) -> WheelMetadataResult { - self.fetcher.get_or_build_wheel_metadata(dist).await + match self.fetcher.get_or_build_wheel_metadata(dist).await { + Ok(metadata) => Ok(MetadataResponse::Found(metadata)), + Err(err) => match err { + uv_distribution::Error::Client(client) => match client.into_kind() { + uv_client::ErrorKind::Offline(_) => Ok(MetadataResponse::Offline), + uv_client::ErrorKind::MetadataParseError(_, _, err) => { + Ok(MetadataResponse::InvalidMetadata(err)) + } + uv_client::ErrorKind::DistInfo(err) => { + Ok(MetadataResponse::InvalidStructure(Box::new(err))) + } + kind => Err(uv_client::Error::from(kind).into()), + }, + uv_distribution::Error::Metadata(err) => { + Ok(MetadataResponse::InvalidMetadata(Box::new(err))) + } + uv_distribution::Error::DistInfo(err) => { + Ok(MetadataResponse::InvalidStructure(Box::new(err))) + } + err => Err(err), + }, + } } fn index_locations(&self) -> &IndexLocations { diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 5f50d655c..909099aa2 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -4420,21 +4420,22 @@ fn offline_registry_backtrack() -> Result<()> { "### ); - // Resolve with `--offline`, with a looser requirement. We should backtrack to `1.1.1`, but - // we don't right now. + // Resolve with `--offline`, with a looser requirement. We should backtrack to `1.1.1`. let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str("iniconfig")?; uv_snapshot!(context.compile() .arg("requirements.in") .arg("--offline"), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --offline + iniconfig==1.1.1 ----- stderr ----- - error: Failed to download: iniconfig==2.0.0 - Caused by: Network connectivity is disabled, but the requested data wasn't found in the cache for: `https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl.metadata` + Resolved 1 package in [TIME] "### ); @@ -4563,16 +4564,15 @@ fn invalid_metadata_requires_python() -> Result<()> { .arg("--find-links") .arg(context.workspace_root.join("scripts").join("links")), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to download: validation==2.0.0 - Caused by: Couldn't parse metadata of validation-2.0.0-py3-none-any.whl from validation==2.0.0 - Caused by: Failed to parse version: Unexpected end of version specifier, expected operator: - 12 - ^^ - + × No solution found when resolving dependencies: + ╰─▶ Because validation==2.0.0 is unusable because its dependencies are + unusable because the package metadata could not be parsed and you + require validation==2.0.0, we can conclude that the requirements are + unsatisfiable. "### ); @@ -4593,12 +4593,15 @@ fn invalid_metadata_multiple_dist_info() -> Result<()> { .arg("--find-links") .arg(context.workspace_root.join("scripts").join("links")), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Failed to download: validation==3.0.0 - Caused by: Multiple .dist-info directories found: validation-2.0.0, validation-3.0.0 + × No solution found when resolving dependencies: + ╰─▶ Because validation==3.0.0 is unusable because its dependencies + are unusable because the package has an invalid format and you + require validation==3.0.0, we can conclude that the requirements are + unsatisfiable. "### ); @@ -4613,19 +4616,21 @@ fn invalid_metadata_backtrack() -> Result<()> { requirements_in.write_str("validation")?; // `2.0.0` and `3.0.0` have invalid metadata. We should backtrack to `1.0.0` (the preceding - // version, which has valid metadata), but we don't right now. + // version, which has valid metadata). uv_snapshot!(context.compile() .arg("requirements.in") .arg("--no-index") .arg("--find-links") .arg(context.workspace_root.join("scripts").join("links")), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --no-index + validation==1.0.0 ----- stderr ----- - error: Failed to download: validation==3.0.0 - Caused by: Multiple .dist-info directories found: validation-2.0.0, validation-3.0.0 + Resolved 1 package in [TIME] "### ); diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 307d425ba..d95380017 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -1100,8 +1100,7 @@ fn mismatched_name() -> Result<()> { ----- stdout ----- ----- stderr ----- - error: Failed to read: foo @ file://[TEMP_DIR]/foo-2.0.1-py3-none-any.whl - Caused by: The .dist-info directory tomli-2.0.1 does not start with the normalized package name: foo + error: Because foo was found, but has an invalid format and you require foo, we can conclude that the requirements are unsatisfiable. "### ); From 8ffdcced0f5c3cf60c289d6adda6205232fdbe63 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Fri, 5 Apr 2024 20:35:00 -0400 Subject: [PATCH 018/110] Use `miette` when printing `pip sync` resolution failures (#2848) Closes https://github.com/astral-sh/uv/issues/2845. --- crates/uv/src/commands/pip_sync.rs | 12 ++++++- crates/uv/tests/pip_sync.rs | 54 ++++++++++++++++++++---------- 2 files changed, 48 insertions(+), 18 deletions(-) diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 28c10c82f..289281614 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -1,5 +1,6 @@ use std::fmt::Write; +use anstream::eprint; use anyhow::{anyhow, Context, Result}; use itertools::Itertools; use owo_colors::OwoColorize; @@ -308,7 +309,16 @@ pub(crate) async fn pip_sync( &EmptyInstalledPackages, )? .with_reporter(reporter); - let resolution = resolver.resolve().await?; + + let resolution = match resolver.resolve().await { + Err(uv_resolver::ResolveError::NoSolution(err)) => { + let report = miette::Report::msg(format!("{err}")) + .context("No solution found when resolving dependencies:"); + eprint!("{report:?}"); + return Ok(ExitStatus::Failure); + } + result => result, + }?; let s = if resolution.len() == 1 { "" } else { "s" }; writeln!( diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index d95380017..1c733cca4 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -838,13 +838,18 @@ fn install_no_index() -> Result<()> { .arg("--no-index") .arg("--strict"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Because markupsafe==2.1.3 was not found in the provided package locations and you require markupsafe==2.1.3, we can conclude that the requirements are unsatisfiable. + × No solution found when resolving dependencies: + ╰─▶ Because markupsafe==2.1.3 was not found in the provided package + locations and you require markupsafe==2.1.3, we can conclude that the + requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) + hint: Packages were unavailable because index lookups were disabled + and no additional package locations were provided (try: `--find-links + `) "### ); @@ -890,13 +895,18 @@ fn install_no_index_cached() -> Result<()> { .arg("--no-index") .arg("--strict"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Because markupsafe==2.1.3 was not found in the provided package locations and you require markupsafe==2.1.3, we can conclude that the requirements are unsatisfiable. + × No solution found when resolving dependencies: + ╰─▶ Because markupsafe==2.1.3 was not found in the provided package + locations and you require markupsafe==2.1.3, we can conclude that the + requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) + hint: Packages were unavailable because index lookups were disabled + and no additional package locations were provided (try: `--find-links + `) "### ); @@ -1096,11 +1106,13 @@ fn mismatched_name() -> Result<()> { .arg("requirements.txt") .arg("--strict"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Because foo was found, but has an invalid format and you require foo, we can conclude that the requirements are unsatisfiable. + × No solution found when resolving dependencies: + ╰─▶ Because foo was found, but has an invalid format and you require foo, we + can conclude that the requirements are unsatisfiable. "### ); @@ -2556,13 +2568,15 @@ fn find_links_offline_no_match() -> Result<()> { .arg("--find-links") .arg(context.workspace_root.join("scripts/links/")), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Because numpy was not found in the cache and you require numpy, we can conclude that the requirements are unsatisfiable. + × No solution found when resolving dependencies: + ╰─▶ Because numpy was not found in the cache and you require numpy, we can + conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because the network was disabled + hint: Packages were unavailable because the network was disabled "### ); @@ -2581,13 +2595,15 @@ fn offline() -> Result<()> { .arg("requirements.in") .arg("--offline"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Because black==23.10.1 was not found in the cache and you require black==23.10.1, we can conclude that the requirements are unsatisfiable. + × No solution found when resolving dependencies: + ╰─▶ Because black==23.10.1 was not found in the cache and you require + black==23.10.1, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because the network was disabled + hint: Packages were unavailable because the network was disabled "### ); @@ -3011,12 +3027,16 @@ requires-python = "<=3.5" uv_snapshot!(context.filters(), command(&context) .arg("requirements.in"), @r###" success: false - exit_code: 2 + exit_code: 1 ----- stdout ----- ----- stderr ----- - error: Because the current Python version (3.12.1) does not satisfy Python<=3.5 and example==0.0.0 depends on Python<=3.5, we can conclude that example==0.0.0 cannot be used. - And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable. + × No solution found when resolving dependencies: + ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.5 + and example==0.0.0 depends on Python<=3.5, we can conclude that + example==0.0.0 cannot be used. + And because only example==0.0.0 is available and you require example, we + can conclude that the requirements are unsatisfiable. "### ); From 35940cb88537dc64907ccf6ab3b6c0b19dfe0d1f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 6 Apr 2024 10:58:23 -0400 Subject: [PATCH 019/110] Remove additional 'because' (#2849) ## Summary Is this, perhaps, not totally necessary? It doesn't show up in any fixtures beyond those that I added recently. Closes https://github.com/astral-sh/uv/issues/2846. --- crates/uv-resolver/src/resolver/mod.rs | 10 +--------- crates/uv/tests/pip_compile.rs | 14 ++++++-------- .../dependent_editables/second_editable/.gitignore | 1 + 3 files changed, 8 insertions(+), 17 deletions(-) diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index b90748b1e..7aaed6ce7 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -429,18 +429,10 @@ impl< .await? { Dependencies::Unavailable(reason) => { - let message = { - if matches!(package, PubGrubPackage::Root(_)) { - // Including front-matter for the root package is redundant - reason.clone() - } else { - format!("its dependencies are unusable because {reason}") - } - }; state.add_incompatibility(Incompatibility::unavailable( package.clone(), version.clone(), - message, + reason.clone(), )); continue; } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 909099aa2..718a3f281 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -4569,10 +4569,9 @@ fn invalid_metadata_requires_python() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because validation==2.0.0 is unusable because its dependencies are - unusable because the package metadata could not be parsed and you - require validation==2.0.0, we can conclude that the requirements are - unsatisfiable. + ╰─▶ Because validation==2.0.0 is unusable because the package metadata could + not be parsed and you require validation==2.0.0, we can conclude that + the requirements are unsatisfiable. "### ); @@ -4598,10 +4597,9 @@ fn invalid_metadata_multiple_dist_info() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because validation==3.0.0 is unusable because its dependencies - are unusable because the package has an invalid format and you - require validation==3.0.0, we can conclude that the requirements are - unsatisfiable. + ╰─▶ Because validation==3.0.0 is unusable because the package has an invalid + format and you require validation==3.0.0, we can conclude that the + requirements are unsatisfiable. "### ); diff --git a/scripts/packages/dependent_editables/second_editable/.gitignore b/scripts/packages/dependent_editables/second_editable/.gitignore index eaa9f051b..c847724a5 100644 --- a/scripts/packages/dependent_editables/second_editable/.gitignore +++ b/scripts/packages/dependent_editables/second_editable/.gitignore @@ -1,2 +1,3 @@ # Artifacts from the build process. *.egg-info/ +build/ From bfc4c1aa5add1528744e2d6ef41e3800592c757d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 6 Apr 2024 19:27:56 -0400 Subject: [PATCH 020/110] Turn off text wrapping in non-scenario fixtures (#2854) ## Summary We do this in the scenarios, but weren't setting it elsewhere. --- crates/uv/tests/cache_prune.rs | 2 + crates/uv/tests/common/mod.rs | 2 + crates/uv/tests/pip_check.rs | 2 + crates/uv/tests/pip_compile.rs | 112 +++++++++++-------------------- crates/uv/tests/pip_freeze.rs | 2 + crates/uv/tests/pip_install.rs | 91 +++++++------------------ crates/uv/tests/pip_list.rs | 18 +++++ crates/uv/tests/pip_show.rs | 11 +++ crates/uv/tests/pip_sync.rs | 34 +++------- crates/uv/tests/pip_uninstall.rs | 2 + crates/uv/tests/venv.rs | 1 + 11 files changed, 116 insertions(+), 161 deletions(-) diff --git a/crates/uv/tests/cache_prune.rs b/crates/uv/tests/cache_prune.rs index fe23ca759..ee8f28c3a 100644 --- a/crates/uv/tests/cache_prune.rs +++ b/crates/uv/tests/cache_prune.rs @@ -21,6 +21,7 @@ fn prune_command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { @@ -41,6 +42,7 @@ fn sync_command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 4d09b7ac7..da179ea4b 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -157,6 +157,7 @@ impl TestContext { .arg("--cache-dir") .arg(self.cache_dir.path()) .env("VIRTUAL_ENV", self.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(self.temp_dir.path()); if cfg!(all(windows, debug_assertions)) { @@ -189,6 +190,7 @@ impl TestContext { .arg("--cache-dir") .arg(self.cache_dir.path()) .env("VIRTUAL_ENV", self.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&self.temp_dir); if cfg!(all(windows, debug_assertions)) { diff --git a/crates/uv/tests/pip_check.rs b/crates/uv/tests/pip_check.rs index 3839b901f..61503a019 100644 --- a/crates/uv/tests/pip_check.rs +++ b/crates/uv/tests/pip_check.rs @@ -21,6 +21,7 @@ fn install_command(context: &TestContext) -> Command { .arg("--exclude-newer") .arg(EXCLUDE_NEWER) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { @@ -41,6 +42,7 @@ fn check_command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); command diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 718a3f281..befa69456 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -882,11 +882,8 @@ fn compile_python_37() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the requested Python version (3.7) does not satisfy Python>=3.8 - and black==23.10.1 depends on Python>=3.8, we can conclude that - black==23.10.1 cannot be used. - And because you require black==23.10.1, we can conclude that the - requirements are unsatisfiable. + ╰─▶ Because the requested Python version (3.7) does not satisfy Python>=3.8 and black==23.10.1 depends on Python>=3.8, we can conclude that black==23.10.1 cannot be used. + And because you require black==23.10.1, we can conclude that the requirements are unsatisfiable. "###); Ok(()) @@ -1458,9 +1455,7 @@ fn conflicting_direct_url_dependency() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because there is no version of werkzeug==3.0.0 and you require - werkzeug==3.0.0, we can conclude that the requirements are - unsatisfiable. + ╰─▶ Because there is no version of werkzeug==3.0.0 and you require werkzeug==3.0.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -1584,10 +1579,8 @@ fn conflicting_transitive_url_dependency() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only werkzeug<3.0.0 is available and flask==3.0.0 depends on - werkzeug>=3.0.0, we can conclude that flask==3.0.0 cannot be used. - And because you require flask==3.0.0, we can conclude that the - requirements are unsatisfiable. + ╰─▶ Because only werkzeug<3.0.0 is available and flask==3.0.0 depends on werkzeug>=3.0.0, we can conclude that flask==3.0.0 cannot be used. + And because you require flask==3.0.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -1936,8 +1929,7 @@ fn requirement_constraint_override_url() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because there is no version of anyio==3.7.0 and you require - anyio==3.7.0, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because there is no version of anyio==3.7.0 and you require anyio==3.7.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -2149,8 +2141,7 @@ dependencies = ["anyio==3.7.0", "anyio==4.0.0"] ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because my-project depends on anyio==3.7.0 and my-project depends on - anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because my-project depends on anyio==3.7.0 and my-project depends on anyio==4.0.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -2181,8 +2172,7 @@ dependencies = ["anyio==300.1.4"] ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because there is no version of anyio==300.1.4 and my-project depends on - anyio==300.1.4, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because there is no version of anyio==300.1.4 and my-project depends on anyio==300.1.4, we can conclude that the requirements are unsatisfiable. "### ); @@ -2207,6 +2197,7 @@ fn compile_exclude_newer() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(context.temp_dir.path()), @r###" success: true exit_code: 0 @@ -2231,6 +2222,7 @@ fn compile_exclude_newer() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(context.temp_dir.path()), @r###" success: true exit_code: 0 @@ -2254,6 +2246,7 @@ fn compile_exclude_newer() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(context.temp_dir.path()), @r###" success: false exit_code: 2 @@ -2628,11 +2621,8 @@ fn compile_yanked_version_indirect() -> Result<()> { attrs<=20.3.0 attrs==21.1.0 attrs>=21.2.0 - and attrs==21.1.0 is unusable because it was yanked (reason: - Installable but not importable on Python 3.4), we can conclude that - attrs>20.3.0,<21.2.0 cannot be used. - And because you require attrs>20.3.0,<21.2.0, we can conclude that the - requirements are unsatisfiable. + and attrs==21.1.0 is unusable because it was yanked (reason: Installable but not importable on Python 3.4), we can conclude that attrs>20.3.0,<21.2.0 cannot be used. + And because you require attrs>20.3.0,<21.2.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -3331,6 +3321,7 @@ fn compile_html() -> Result<()> { .arg("--index-url") .arg("https://download.pytorch.org/whl") .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(context.temp_dir.path()), @r###" success: true exit_code: 0 @@ -4225,12 +4216,9 @@ fn no_index_requirements_txt() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because tqdm was not found in the provided package locations and you - require tqdm, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because tqdm was not found in the provided package locations and you require tqdm, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "### ); @@ -4336,8 +4324,7 @@ fn offline_registry() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because black==23.10.1 was not found in the cache and you require - black==23.10.1, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because black==23.10.1 was not found in the cache and you require black==23.10.1, we can conclude that the requirements are unsatisfiable. hint: Packages were unavailable because the network was disabled "### @@ -4464,8 +4451,7 @@ fn offline_find_links() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because tqdm was not found in the cache and you require tqdm, we can - conclude that the requirements are unsatisfiable. + ╰─▶ Because tqdm was not found in the cache and you require tqdm, we can conclude that the requirements are unsatisfiable. hint: Packages were unavailable because the network was disabled "### @@ -4484,8 +4470,7 @@ fn offline_find_links() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because tqdm was not found in the cache and you require tqdm, we can - conclude that the requirements are unsatisfiable. + ╰─▶ Because tqdm was not found in the cache and you require tqdm, we can conclude that the requirements are unsatisfiable. hint: Packages were unavailable because the network was disabled "### @@ -4569,9 +4554,7 @@ fn invalid_metadata_requires_python() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because validation==2.0.0 is unusable because the package metadata could - not be parsed and you require validation==2.0.0, we can conclude that - the requirements are unsatisfiable. + ╰─▶ Because validation==2.0.0 is unusable because the package metadata could not be parsed and you require validation==2.0.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -4597,9 +4580,7 @@ fn invalid_metadata_multiple_dist_info() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because validation==3.0.0 is unusable because the package has an invalid - format and you require validation==3.0.0, we can conclude that the - requirements are unsatisfiable. + ╰─▶ Because validation==3.0.0 is unusable because the package has an invalid format and you require validation==3.0.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -4814,8 +4795,7 @@ fn compile_constraints_incompatible_url() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only anyio>=4 is available and you require anyio<4, we can - conclude that the requirements are unsatisfiable. + ╰─▶ Because only anyio>=4 is available and you require anyio<4, we can conclude that the requirements are unsatisfiable. "### ); @@ -4838,8 +4818,7 @@ fn index_url_in_requirements() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because anyio<4 was not found in the package registry and you require - anyio<4, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because anyio<4 was not found in the package registry and you require anyio<4, we can conclude that the requirements are unsatisfiable. "### ); @@ -5173,8 +5152,7 @@ fn compile_constraints_incompatible_version() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because you require filelock==1.0.0 and you require filelock==3.8.0, we - can conclude that the requirements are unsatisfiable. + ╰─▶ Because you require filelock==1.0.0 and you require filelock==3.8.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -5202,8 +5180,7 @@ fn conflicting_url_markers() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because you require filelock==1.0.0 and you require filelock==3.8.0, we - can conclude that the requirements are unsatisfiable. + ╰─▶ Because you require filelock==1.0.0 and you require filelock==3.8.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -5351,8 +5328,7 @@ fn override_with_incompatible_constraint() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because you require anyio>=3.0.0 and you require anyio<3.0.0, we can - conclude that the requirements are unsatisfiable. + ╰─▶ Because you require anyio>=3.0.0 and you require anyio<3.0.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -6015,6 +5991,7 @@ fn no_stream() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -6087,11 +6064,8 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.8 - and example==0.0.0 depends on Python<=3.8, we can conclude that - example==0.0.0 cannot be used. - And because only example==0.0.0 is available and you require example, we - can conclude that the requirements are unsatisfiable. + ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable. "### ); @@ -6483,7 +6457,7 @@ fn unnamed_path_requirement() -> Result<()> { fn unnamed_git_requirement() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("git+https://github.com/pallets/flask.git")?; + requirements_in.write_str("git+https://github.com/pallets/flask.git@3.0.0")?; uv_snapshot!(context.compile() .arg("requirements.in"), @r###" @@ -6496,7 +6470,7 @@ fn unnamed_git_requirement() -> Result<()> { # via flask click==8.1.7 # via flask - flask @ git+https://github.com/pallets/flask.git@b90a4f1f4a370e92054b9cc9db0efcb864f87ebe + flask @ git+https://github.com/pallets/flask.git@735a4701d6d5e848241e7d7535db898efb62d400 itsdangerous==2.1.2 # via flask jinja2==3.1.3 @@ -7105,19 +7079,16 @@ requires-python = ">3.8" .arg("requirements.in") .arg("--override") .arg("overrides.txt"), @r###" - success: false - exit_code: 1 - ----- stdout ----- + success: false + exit_code: 1 + ----- stdout ----- - ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because there is no version of anyio==0.0.0 and lib==0.0.0 depends on - anyio==0.0.0, we can conclude that lib==0.0.0 cannot be used. - And because only lib==0.0.0 is available and example==0.0.0 depends on - lib, we can conclude that example==0.0.0 cannot be used. - And because only example==0.0.0 is available and you require example, we - can conclude that the requirements are unsatisfiable. - "### + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because there is no version of anyio==0.0.0 and lib==0.0.0 depends on anyio==0.0.0, we can conclude that lib==0.0.0 cannot be used. + And because only lib==0.0.0 is available and example==0.0.0 depends on lib, we can conclude that example==0.0.0 cannot be used. + And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable. + "### ); // Now constrain `anyio` to the local version. @@ -7283,8 +7254,7 @@ fn compile_index_url_first_match() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because there is no version of jinja2==3.1.0 and you require - jinja2==3.1.0, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because there is no version of jinja2==3.1.0 and you require jinja2==3.1.0, we can conclude that the requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_freeze.rs b/crates/uv/tests/pip_freeze.rs index d3e5b4582..71da76dc9 100644 --- a/crates/uv/tests/pip_freeze.rs +++ b/crates/uv/tests/pip_freeze.rs @@ -19,6 +19,7 @@ fn command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); command } @@ -32,6 +33,7 @@ fn sync_command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index acfa3c947..7a153913c 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -267,11 +267,8 @@ fn no_solution() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because only flask<=3.0.2 is available and flask==3.0.2 depends - on werkzeug>=3.0.0, we can conclude that flask>=3.0.2 depends on - werkzeug>=3.0.0. - And because you require flask>=3.0.2 and you require werkzeug<1.0.0, we - can conclude that the requirements are unsatisfiable. + ╰─▶ Because only flask<=3.0.2 is available and flask==3.0.2 depends on werkzeug>=3.0.0, we can conclude that flask>=3.0.2 depends on werkzeug>=3.0.0. + And because you require flask>=3.0.2 and you require werkzeug<1.0.0, we can conclude that the requirements are unsatisfiable. "###); } @@ -946,12 +943,9 @@ fn install_no_index() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because flask was not found in the provided package locations and you - require flask, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because flask was not found in the provided package locations and you require flask, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "### ); @@ -973,13 +967,9 @@ fn install_no_index_version() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because flask==3.0.0 was not found in the provided package locations - and you require flask==3.0.0, we can conclude that the requirements - are unsatisfiable. + ╰─▶ Because flask==3.0.0 was not found in the provided package locations and you require flask==3.0.0, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "### ); @@ -1372,10 +1362,7 @@ fn only_binary_requirements_txt() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because django-allauth==0.51.0 is unusable because no wheels - are usable and building from source is disabled and you require - django-allauth==0.51.0, we can conclude that the requirements are - unsatisfiable. + ╰─▶ Because django-allauth==0.51.0 is unusable because no wheels are usable and building from source is disabled and you require django-allauth==0.51.0, we can conclude that the requirements are unsatisfiable. "### ); } @@ -2885,11 +2872,8 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.8 - and example==0.0.0 depends on Python<=3.8, we can conclude that - example==0.0.0 cannot be used. - And because only example==0.0.0 is available and you require example, we - can conclude that the requirements are unsatisfiable. + ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable. "### ); @@ -3108,12 +3092,9 @@ fn reinstall_no_index() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because anyio was not found in the provided package locations and you - require anyio, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because anyio was not found in the provided package locations and you require anyio, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "### ); } @@ -3229,12 +3210,8 @@ fn already_installed_dependent_editable() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because first-editable was not found in the provided package locations - and second-editable==0.0.1 depends on first-editable, we can conclude - that second-editable==0.0.1 cannot be used. - And because only second-editable==0.0.1 is available and you - require second-editable, we can conclude that the requirements are - unsatisfiable. + ╰─▶ Because first-editable was not found in the provided package locations and second-editable==0.0.1 depends on first-editable, we can conclude that second-editable==0.0.1 cannot be used. + And because only second-editable==0.0.1 is available and you require second-editable, we can conclude that the requirements are unsatisfiable. "### ); @@ -3329,11 +3306,8 @@ fn already_installed_local_path_dependent() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because first-local was not found in the provided package locations - and second-local==0.1.0 depends on first-local, we can conclude that - second-local==0.1.0 cannot be used. - And because only second-local==0.1.0 is available and you require - second-local, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. + And because only second-local==0.1.0 is available and you require second-local, we can conclude that the requirements are unsatisfiable. "### ); @@ -3372,11 +3346,8 @@ fn already_installed_local_path_dependent() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because first-local was not found in the provided package locations - and second-local==0.1.0 depends on first-local, we can conclude that - second-local==0.1.0 cannot be used. - And because only second-local==0.1.0 is available and you require - second-local, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because first-local was not found in the provided package locations and second-local==0.1.0 depends on first-local, we can conclude that second-local==0.1.0 cannot be used. + And because only second-local==0.1.0 is available and you require second-local, we can conclude that the requirements are unsatisfiable. "### ); @@ -3447,13 +3418,9 @@ fn already_installed_local_version_of_remote_package() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because anyio==4.2.0 was not found in the provided package locations - and you require anyio==4.2.0, we can conclude that the requirements - are unsatisfiable. + ╰─▶ Because anyio==4.2.0 was not found in the provided package locations and you require anyio==4.2.0, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "### ); @@ -3468,9 +3435,7 @@ fn already_installed_local_version_of_remote_package() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because there is no version of anyio==4.3.0+foo and you require - anyio==4.3.0+foo, we can conclude that the requirements are - unsatisfiable. + ╰─▶ Because there is no version of anyio==4.3.0+foo and you require anyio==4.3.0+foo, we can conclude that the requirements are unsatisfiable. "### ); @@ -3681,13 +3646,9 @@ fn already_installed_remote_url() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because uv-public-pypackage was not found in the provided package - locations and you require uv-public-pypackage, we can conclude that the - requirements are unsatisfiable. + ╰─▶ Because uv-public-pypackage was not found in the provided package locations and you require uv-public-pypackage, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "###); // Request installation again with just the full URL @@ -3730,13 +3691,9 @@ fn already_installed_remote_url() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because uv-public-pypackage==0.2.0 was not found in the provided package - locations and you require uv-public-pypackage==0.2.0, we can conclude - that the requirements are unsatisfiable. + ╰─▶ Because uv-public-pypackage==0.2.0 was not found in the provided package locations and you require uv-public-pypackage==0.2.0, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "###); } diff --git a/crates/uv/tests/pip_list.rs b/crates/uv/tests/pip_list.rs index 9b545ee2c..e738f96c6 100644 --- a/crates/uv/tests/pip_list.rs +++ b/crates/uv/tests/pip_list.rs @@ -21,6 +21,7 @@ fn install_command(context: &TestContext) -> Command { .arg("--exclude-newer") .arg(EXCLUDE_NEWER) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { @@ -42,6 +43,7 @@ fn list_empty() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -84,6 +86,7 @@ fn list_single_no_editable() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -135,6 +138,7 @@ fn list_editable() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -188,6 +192,7 @@ fn list_editable_only() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -207,6 +212,7 @@ fn list_editable_only() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -229,6 +235,7 @@ fn list_editable_only() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -277,6 +284,7 @@ fn list_exclude() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -300,6 +308,7 @@ fn list_exclude() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -324,6 +333,7 @@ fn list_exclude() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -377,6 +387,7 @@ fn list_format_json() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -395,6 +406,7 @@ fn list_format_json() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -413,6 +425,7 @@ fn list_format_json() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -432,6 +445,7 @@ fn list_format_json() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -479,6 +493,7 @@ fn list_format_freeze() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -500,6 +515,7 @@ fn list_format_freeze() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -518,6 +534,7 @@ fn list_format_freeze() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -539,6 +556,7 @@ fn list_format_freeze() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/pip_show.rs index 5edee570d..2d3cc984e 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/pip_show.rs @@ -24,6 +24,7 @@ fn install_command(context: &TestContext) -> Command { .arg("--exclude-newer") .arg(EXCLUDE_NEWER) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { @@ -45,6 +46,7 @@ fn show_empty() { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: false exit_code: 1 @@ -92,6 +94,7 @@ fn show_requires_multiple() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -149,6 +152,7 @@ fn show_python_version_marker() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -199,6 +203,7 @@ fn show_found_single_package() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -255,6 +260,7 @@ fn show_found_multiple_packages() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -318,6 +324,7 @@ fn show_found_one_out_of_three() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -377,6 +384,7 @@ fn show_found_one_out_of_two_quiet() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -429,6 +437,7 @@ fn show_empty_quiet() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: false exit_code: 1 @@ -464,6 +473,7 @@ fn show_editable() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 @@ -526,6 +536,7 @@ fn show_required_by_multiple() -> Result<()> { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir), @r###" success: true exit_code: 0 diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 1c733cca4..3f15bb13d 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -41,6 +41,7 @@ fn command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { @@ -61,6 +62,7 @@ fn uninstall_command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { @@ -843,13 +845,9 @@ fn install_no_index() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because markupsafe==2.1.3 was not found in the provided package - locations and you require markupsafe==2.1.3, we can conclude that the - requirements are unsatisfiable. + ╰─▶ Because markupsafe==2.1.3 was not found in the provided package locations and you require markupsafe==2.1.3, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "### ); @@ -900,13 +898,9 @@ fn install_no_index_cached() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because markupsafe==2.1.3 was not found in the provided package - locations and you require markupsafe==2.1.3, we can conclude that the - requirements are unsatisfiable. + ╰─▶ Because markupsafe==2.1.3 was not found in the provided package locations and you require markupsafe==2.1.3, we can conclude that the requirements are unsatisfiable. - hint: Packages were unavailable because index lookups were disabled - and no additional package locations were provided (try: `--find-links - `) + hint: Packages were unavailable because index lookups were disabled and no additional package locations were provided (try: `--find-links `) "### ); @@ -1111,8 +1105,7 @@ fn mismatched_name() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because foo was found, but has an invalid format and you require foo, we - can conclude that the requirements are unsatisfiable. + ╰─▶ Because foo was found, but has an invalid format and you require foo, we can conclude that the requirements are unsatisfiable. "### ); @@ -2573,8 +2566,7 @@ fn find_links_offline_no_match() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because numpy was not found in the cache and you require numpy, we can - conclude that the requirements are unsatisfiable. + ╰─▶ Because numpy was not found in the cache and you require numpy, we can conclude that the requirements are unsatisfiable. hint: Packages were unavailable because the network was disabled "### @@ -2600,8 +2592,7 @@ fn offline() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because black==23.10.1 was not found in the cache and you require - black==23.10.1, we can conclude that the requirements are unsatisfiable. + ╰─▶ Because black==23.10.1 was not found in the cache and you require black==23.10.1, we can conclude that the requirements are unsatisfiable. hint: Packages were unavailable because the network was disabled "### @@ -3032,11 +3023,8 @@ requires-python = "<=3.5" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.5 - and example==0.0.0 depends on Python<=3.5, we can conclude that - example==0.0.0 cannot be used. - And because only example==0.0.0 is available and you require example, we - can conclude that the requirements are unsatisfiable. + ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.5 and example==0.0.0 depends on Python<=3.5, we can conclude that example==0.0.0 cannot be used. + And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_uninstall.rs b/crates/uv/tests/pip_uninstall.rs index df3f44251..84349b184 100644 --- a/crates/uv/tests/pip_uninstall.rs +++ b/crates/uv/tests/pip_uninstall.rs @@ -19,6 +19,7 @@ fn uninstall_command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { @@ -39,6 +40,7 @@ fn sync_command(context: &TestContext) -> Command { .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/venv.rs index 94e79ae79..1e195708e 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/venv.rs @@ -482,6 +482,7 @@ fn verify_nested_pyvenv_cfg() -> Result<()> { .arg("--python") .arg("3.12") .env("VIRTUAL_ENV", context.venv.as_os_str()) + .env("UV_NO_WRAP", "1") .assert() .success(); From fee9657548dd05ca27395b993bef70da4d6771cc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 6 Apr 2024 20:36:41 -0400 Subject: [PATCH 021/110] Only ignore `bin` directory at repo root (#2856) Closes https://github.com/astral-sh/uv/issues/1304. --- .gitignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index b195ac35c..1a106a64e 100644 --- a/.gitignore +++ b/.gitignore @@ -8,7 +8,7 @@ target/ target-alpine/ # Bootstrapped Python versions -bin/ +/bin/ # These are backup files generated by rustfmt **/*.rs.bk From a3de0e8bc6ce7bc13f1e0b5329e84275b1e71da8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 6 Apr 2024 20:42:30 -0400 Subject: [PATCH 022/110] Include LICENSE files in source distribution (#2855) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Closes https://github.com/astral-sh/uv/issues/2724. ## Test Plan Ran `maturin sdist`: ![Screenshot 2024-04-06 at 8 08 38 PM](https://github.com/astral-sh/uv/assets/1309177/48ef10d2-407a-4280-b7c0-e38591a28317) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 35549781d..e1d38c54e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -41,7 +41,7 @@ manifest-path = "crates/uv/Cargo.toml" module-name = "uv" python-source = "python" strip = true -include = ["rust-toolchain.toml"] +include = [{ path = "rust-toolchain.toml", format = ["sdist", "wheel"] }, { path = "LICENSE-APACHE", format = "sdist" }, { path = "LICENSE-MIT", format = "sdist" }] [tool.rooster] major_labels = [] # We do not use the major version number From d4a258422bd13abbb647e7e443036d0d3193f0cc Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 6 Apr 2024 22:12:03 -0400 Subject: [PATCH 023/110] DRY up request builer in wheel download (#2857) --- .../src/distribution_database.rs | 43 ++++++++----------- 1 file changed, 17 insertions(+), 26 deletions(-) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index dbe968067..3b22dfbaa 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -413,18 +413,6 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .instrument(info_span!("wheel", wheel = %dist)) }; - let req = self - .client - .uncached_client() - .get(url) - .header( - // `reqwest` defaults to accepting compressed responses. - // Specify identity encoding to get consistent .whl downloading - // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 - "accept-encoding", - reqwest::header::HeaderValue::from_static("identity"), - ) - .build()?; let cache_control = match self.client.connectivity() { Connectivity::Online => CacheControl::from( self.build_context @@ -438,7 +426,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let archive = self .client .cached_client() - .get_serde(req, &http_entry, cache_control, download) + .get_serde(self.request(url)?, &http_entry, cache_control, download) .await .map_err(|err| match err { CachedClientError::Callback(err) => err, @@ -495,18 +483,6 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .instrument(info_span!("wheel", wheel = %dist)) }; - let req = self - .client - .uncached_client() - .get(url) - .header( - // `reqwest` defaults to accepting compressed responses. - // Specify identity encoding to get consistent .whl downloading - // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 - "accept-encoding", - reqwest::header::HeaderValue::from_static("identity"), - ) - .build()?; let cache_control = match self.client.connectivity() { Connectivity::Online => CacheControl::from( self.build_context @@ -520,7 +496,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let archive = self .client .cached_client() - .get_serde(req, &http_entry, cache_control, download) + .get_serde(self.request(url)?, &http_entry, cache_control, download) .await .map_err(|err| match err { CachedClientError::Callback(err) => err, @@ -530,6 +506,21 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> Ok(archive) } + /// Returns a GET [`reqwest::Request`] for the given URL. + fn request(&self, url: Url) -> Result { + self.client + .uncached_client() + .get(url) + .header( + // `reqwest` defaults to accepting compressed responses. + // Specify identity encoding to get consistent .whl downloading + // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 + "accept-encoding", + reqwest::header::HeaderValue::from_static("identity"), + ) + .build() + } + /// Return the [`IndexLocations`] used by this resolver. pub fn index_locations(&self) -> &IndexLocations { self.build_context.index_locations() From 424d7ab610ace73103a748713a4b2c148fa550e4 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 6 Apr 2024 23:25:54 -0400 Subject: [PATCH 024/110] Sort and expand target triples in Cargo.toml (#2858) ## Summary Too hard to read as-is. --- Cargo.toml | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3621f00fd..d857df461 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -207,8 +207,24 @@ 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-unknown-linux-gnu", "x86_64-unknown-linux-gnu", "i686-unknown-linux-gnu", "aarch64-apple-darwin", "x86_64-apple-darwin", "aarch64-unknown-linux-musl", "x86_64-unknown-linux-musl", "i686-unknown-linux-musl", "x86_64-pc-windows-msvc", "i686-pc-windows-msvc", "armv7-unknown-linux-gnueabihf", "powerpc64-unknown-linux-gnu", "powerpc64le-unknown-linux-gnu", "s390x-unknown-linux-gnu", "armv7-unknown-linux-musleabihf", "arm-unknown-linux-musleabihf"] -# Whether to auto-include files like READMEs and CHANGELOGs (default true) +targets = [ + "aarch64-apple-darwin", + "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", + "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 and CHANGELOGs (default true) auto-includes = false # Whether cargo-dist should create a Github Release or use an existing draft create-release = true From a217752cacc35d56d3eb5b03b8b8b3a0bb584f5b Mon Sep 17 00:00:00 2001 From: Jelle Zijlstra Date: Sun, 7 Apr 2024 08:34:02 -0400 Subject: [PATCH 025/110] PIP_COMPATIBILITY.md: typo fixes (#2859) --- PIP_COMPATIBILITY.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/PIP_COMPATIBILITY.md b/PIP_COMPATIBILITY.md index 77bde9115..594c60096 100644 --- a/PIP_COMPATIBILITY.md +++ b/PIP_COMPATIBILITY.md @@ -30,7 +30,7 @@ drawbacks: target tool the user is expecting to use. 4. It prevents uv from introducing any settings or configuration that don't exist in the target tool, since otherwise `pip.conf` (or similar) would no longer be usable with `pip`. -5. It can lead user confusion, since uv would be reading settings that don't actually affect its +5. It can lead to user confusion, since uv would be reading settings that don't actually affect its behavior, and many users may _not_ expect uv to read configuration files intended for other tools. @@ -107,7 +107,7 @@ the available versions of a given package. However, uv and `pip` differ in how t packages that exist on multiple indexes. For example, imagine that a company publishes an internal version of `requests` on a private index -(`--extra-index-url`), but also allow installing packages from PyPI by default. In this case, the +(`--extra-index-url`), but also allows installing packages from PyPI by default. In this case, the private `requests` would conflict with the public [`requests`](https://pypi.org/project/requests/) on PyPI. @@ -117,7 +117,7 @@ finds a match. This means that if a package exists on multiple indexes, uv will candidate versions to those present in the first index that contains the package. `pip`, meanwhile, will combine the candidate versions from all indexes, and select the best -version from the combined set., though it makes [no guarantees around the order](https://github.com/pypa/pip/issues/5045#issuecomment-369521345) +version from the combined set, though it makes [no guarantees around the order](https://github.com/pypa/pip/issues/5045#issuecomment-369521345) in which it searches indexes, and expects that packages are unique up to name and version, even across indexes. From b535d252c9570e395ada84f8bfab0bb3723efc5a Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sun, 7 Apr 2024 11:16:13 -0500 Subject: [PATCH 026/110] Fix base client builder docstring reference (#2860) --- crates/uv-client/src/base_client.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index c8a9f424d..c45974d48 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -19,7 +19,7 @@ use crate::middleware::OfflineMiddleware; use crate::tls::Roots; use crate::{tls, Connectivity}; -/// A builder for an [`RegistryClient`]. +/// A builder for an [`BaseClient`]. #[derive(Debug, Clone)] pub struct BaseClientBuilder<'a> { keyring_provider: KeyringProvider, From d34c90150d864302cea184566277b636e58625dc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 02:28:04 +0000 Subject: [PATCH 027/110] Update Rust crate async-compression to v0.4.8 (#2865) --- Cargo.lock | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 985298c32..3b2f86833 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -200,9 +200,9 @@ dependencies = [ [[package]] name = "async-compression" -version = "0.4.6" +version = "0.4.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a116f46a969224200a0a97f29cfd4c50e7534e4b4826bd23ea2c3c533039c82c" +checksum = "07dbbf24db18d609b1462965249abdf49129ccad073ec257da372adc83259c60" dependencies = [ "brotli", "flate2", @@ -410,9 +410,9 @@ dependencies = [ [[package]] name = "brotli" -version = "3.4.0" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "516074a47ef4bce09577a3b379392300159ce5b1ba2e501ff1c819950066100f" +checksum = "125740193d7fee5cc63ab9e16c2fdc4e07c74ba755cc53b327d6ea029e9fc569" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -421,9 +421,9 @@ dependencies = [ [[package]] name = "brotli-decompressor" -version = "2.5.1" +version = "3.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e2e4afe60d7dd600fdd3de8d0f08c2b7ec039712e3b6137ff98b7004e82de4f" +checksum = "65622a320492e09b5e0ac436b14c54ff68199bac392d0e89a6832c4518eea525" dependencies = [ "alloc-no-stdlib", "alloc-stdlib", @@ -2670,7 +2670,7 @@ dependencies = [ "indoc", "libc", "memoffset 0.9.0", - "parking_lot 0.12.1", + "parking_lot 0.11.2", "portable-atomic", "pyo3-build-config", "pyo3-ffi", From 8e7f9b18c5b8b307f04e184b3b7d628049194d02 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 02:28:21 +0000 Subject: [PATCH 028/110] Update Rust crate git2 to v0.18.3 (#2867) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 3b2f86833..cbdafbf2d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1356,9 +1356,9 @@ checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" [[package]] name = "git2" -version = "0.18.2" +version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1b3ba52851e73b46a4c3df1d89343741112003f0f6f13beb0dfac9e457c3fdcd" +checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" dependencies = [ "bitflags 2.4.2", "libc", From 14ef0e044cd77f44ca6d6ea4517345ec1c1f6da7 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 02:28:47 +0000 Subject: [PATCH 029/110] Update Rust crate regex to v1.10.4 (#2870) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cbdafbf2d..c0f0d3e06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2882,9 +2882,9 @@ dependencies = [ [[package]] name = "regex" -version = "1.10.3" +version = "1.10.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b62dbe01f0b06f9d8dc7d49e05a0785f153b00b2c227856282f671e0318c9b15" +checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c" dependencies = [ "aho-corasick", "memchr", From 78d64bfc86b1e45c0c2c0745b8ac84d4f486c692 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 02:29:03 +0000 Subject: [PATCH 030/110] Update Rust crate axoupdater to v0.3.6 (#2866) --- Cargo.lock | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index c0f0d3e06..d00731034 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -266,9 +266,9 @@ checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" [[package]] name = "axoasset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dce2f189800bafe8322ef3a4d361ee7373bfc2f8fe052afda404230166dc45f" +checksum = "5e05853b0d9abfab8e7532cad0d07ec396dd95c1a81926b49ab3cfa121a9d8d6" dependencies = [ "camino", "image", @@ -294,9 +294,9 @@ dependencies = [ [[package]] name = "axoupdater" -version = "0.3.3" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e18b628756d7e73bcd3b7330e5834a44f841b115e92bad8563c3dc616a64131" +checksum = "01f40156112045abb1409e115d684306b9ff005399fd10ce2caf33bd800742b4" dependencies = [ "axoasset", "axoprocess", @@ -3674,9 +3674,9 @@ dependencies = [ [[package]] name = "temp-dir" -version = "0.1.12" +version = "0.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd16aa9ffe15fe021c6ee3766772132c6e98dfa395a167e16864f61a9cfb71d6" +checksum = "1f227968ec00f0e5322f9b8173c7a0cbcff6181a0a5b28e9892491c286277231" [[package]] name = "tempfile" From 56555b991125808e8436c765a9d51419c6baecfc Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:37:39 -0400 Subject: [PATCH 031/110] Update Rust crate indexmap to v2.2.6 (#2868) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d00731034..7f1f8213d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1745,9 +1745,9 @@ checksum = "b72ad49b554c1728b1e83254a1b1565aea4161e28dabbfa171fc15fe62299caf" [[package]] name = "indexmap" -version = "2.2.5" +version = "2.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" +checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26" dependencies = [ "equivalent", "hashbrown 0.14.3", From a4bbe6602f5e35b8f14bbc64128f6b232d52852f Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:37:51 -0400 Subject: [PATCH 032/110] Update Rust crate serde_json to v1.0.115 (#2871) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7f1f8213d..abd25c670 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3362,9 +3362,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.114" +version = "1.0.115" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5f09b1bd632ef549eaa9f60a1f8de742bdbc698e6cee2095fc84dde5f549ae0" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" dependencies = [ "itoa", "ryu", From 74371ede8d9f6eb5002ec190e823b902f2c88dd1 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:37:59 -0400 Subject: [PATCH 033/110] Update Rust crate tokio-stream to v0.1.15 (#2872) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index abd25c670..e07a717ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3907,9 +3907,9 @@ dependencies = [ [[package]] name = "tokio-stream" -version = "0.1.14" +version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "397c988d37662c7dda6d2208364a706264bf3d6138b11d436cbac0ad38832842" +checksum = "267ac89e0bec6e691e5813911606935d77c476ff49024f98abcea3e7b15e37af" dependencies = [ "futures-core", "pin-project-lite", From f5334a4914a0e0fbe146a3243a60c725e736d116 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:38:15 -0400 Subject: [PATCH 034/110] Update Rust crate which to v6.0.1 (#2873) --- Cargo.lock | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index e07a717ac..ad109935d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5037,15 +5037,14 @@ checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" [[package]] name = "which" -version = "6.0.0" +version = "6.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fa5e0c10bf77f44aac573e498d1a82d5fbd5e91f6fc0a99e7be4b38e85e101c" +checksum = "8211e4f58a2b2805adfbefbc07bab82958fc91e3836339b1ab7ae32465dce0d7" dependencies = [ "either", "home", - "once_cell", "rustix", - "windows-sys 0.52.0", + "winsafe", ] [[package]] @@ -5308,6 +5307,12 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "wiremock" version = "0.6.0" From e109e02b125615f45b41072c57aa63eb03850a32 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 7 Apr 2024 22:48:12 -0400 Subject: [PATCH 035/110] Expand documentation in `scripts/bench/__main__.py` (#2875) Closes https://github.com/astral-sh/uv/issues/1318. --- scripts/bench/__main__.py | 48 +++++++++++++++++++++++++++++++++------ 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/scripts/bench/__main__.py b/scripts/bench/__main__.py index a08c1ff31..8e87c262f 100644 --- a/scripts/bench/__main__.py +++ b/scripts/bench/__main__.py @@ -1,11 +1,11 @@ """Benchmark uv against other packaging tools. -This script assumes that `pip`, `pip-tools`, `virtualenv`, `poetry` and `hyperfine` are -installed, and that a uv release builds exists at `./target/release/uv` -(relative to the repository root). - This script assumes that Python 3.12 is installed. +By default, this script also assumes that `pip`, `pip-tools`, `virtualenv`, `poetry` and +`hyperfine` are installed, and that a uv release builds exists at `./target/release/uv` +(relative to the repository root). However, the set of tools is configurable. + To set up the required environment, run: cargo build --release @@ -13,17 +13,51 @@ To set up the required environment, run: source .venv/bin/activate ./target/release/uv pip sync ./scripts/bench/requirements.txt -Example usage: +Then, to benchmark uv against `pip-tools`: python -m scripts.bench --uv --pip-compile requirements.in -Multiple versions of uv can be benchmarked by specifying the path to the binary for -each build, as in: +It's most common to benchmark multiple uv versions against one another by building +from multiple branches and specifying the path to each binary, as in: + # Build the baseline version. + git checkout main + cargo build --release + mv ./target/release/uv ./target/release/baseline + + # Build the feature version. + git checkout feature + cargo build --release + + # Run the benchmark. python -m scripts.bench \ --uv-path ./target/release/uv \ --uv-path ./target/release/baseline \ requirements.in + +By default, the script will run the resolution benchmarks when a `requirements.in` file +is provided, and the installation benchmarks when a `requirements.txt` file is provided: + + # Run the resolution benchmarks against the Trio project. + python -m scripts.bench \ + --uv-path ./target/release/uv \ + --uv-path ./target/release/baseline \ + ./scripts/requirements/trio.in + + # Run the installation benchmarks against the Trio project. + python -m scripts.bench \ + --uv-path ./target/release/uv \ + --uv-path ./target/release/baseline \ + ./scripts/requirements/compiled/trio.txt + +You can also specify the benchmark to run explicitly: + + # Run the "uncached install" benchmark against the Trio project. + python -m scripts.bench \ + --uv-path ./target/release/uv \ + --uv-path ./target/release/baseline \ + --benchmark install-cold \ + ./scripts/requirements/compiled/trio.txt """ import abc From 52577892ebcd4cdd8d7d731a529834b65c5673e8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sun, 7 Apr 2024 22:50:24 -0400 Subject: [PATCH 036/110] Expand some documentation around identifier traits (#2876) ## Summary I already added more documentation since this issue was created, but this doesn't hurt. Closes https://github.com/astral-sh/uv/issues/496. --- crates/distribution-types/src/id.rs | 18 ++++++++++++++++-- crates/distribution-types/src/traits.rs | 13 ++++++++++++- 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/crates/distribution-types/src/id.rs b/crates/distribution-types/src/id.rs index c6e2a2a8b..9920ae2d5 100644 --- a/crates/distribution-types/src/id.rs +++ b/crates/distribution-types/src/id.rs @@ -5,10 +5,12 @@ use url::Url; use pep440_rs::Version; use uv_normalize::PackageName; -/// A unique identifier for a package (e.g., `black==23.10.0`). +/// A unique identifier for a package at a specific version (e.g., `black==23.10.0`). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum PackageId { + /// The identifier consists of a package name and version. NameVersion(PackageName, Version), + /// The identifier consists of a URL. Url(String), } @@ -33,7 +35,19 @@ impl Display for PackageId { } } -/// A unique identifier for a distribution (e.g., `black-23.10.0-py3-none-any.whl`). +/// A unique resource identifier for the distribution, like a SHA-256 hash of the distribution's +/// contents. +/// +/// A distribution is a specific archive of a package at a specific version. For a given package +/// version, there may be multiple distributions, e.g., source distribution, along with +/// multiple binary distributions (wheels) for different platforms. As a concrete example, +/// `black-23.10.0-py3-none-any.whl` would represent a (binary) distribution of the `black` package +/// at version `23.10.0`. +/// +/// The distribution ID is used to uniquely identify a distribution. Ideally, the distribution +/// ID should be a hash of the distribution's contents, though in practice, it's only required +/// that the ID is unique within a single invocation of the resolver (and so, e.g., a hash of +/// the URL would also be sufficient). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct DistributionId(String); diff --git a/crates/distribution-types/src/traits.rs b/crates/distribution-types/src/traits.rs index 4f72961bd..c5beacbf5 100644 --- a/crates/distribution-types/src/traits.rs +++ b/crates/distribution-types/src/traits.rs @@ -25,7 +25,7 @@ pub trait DistributionMetadata: Name { /// for URL-based distributions. fn version_or_url(&self) -> VersionOrUrl; - /// Returns a unique identifier for the package. + /// Returns a unique identifier for the package (e.g., `black==23.10.0`). /// /// Note that this is not equivalent to a unique identifier for the _distribution_, as multiple /// registry-based distributions (e.g., different wheels for the same package and version) @@ -57,6 +57,17 @@ pub trait RemoteSource { pub trait Identifier { /// Return a unique resource identifier for the distribution, like a SHA-256 hash of the /// distribution's contents. + /// + /// A distribution is a specific archive of a package at a specific version. For a given package + /// version, there may be multiple distributions, e.g., source distribution, along with + /// multiple binary distributions (wheels) for different platforms. As a concrete example, + /// `black-23.10.0-py3-none-any.whl` would represent a (binary) distribution of the `black` package + /// at version `23.10.0`. + /// + /// The distribution ID is used to uniquely identify a distribution. Ideally, the distribution + /// ID should be a hash of the distribution's contents, though in practice, it's only required + /// that the ID is unique within a single invocation of the resolver (and so, e.g., a hash of + /// the URL would also be sufficient). fn distribution_id(&self) -> DistributionId; /// Return a unique resource identifier for the underlying resource backing the distribution. From f0c83a4deddd8045be273aa2e3d416124365c858 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 03:02:35 +0000 Subject: [PATCH 037/110] Update Rust crate base64 to 0.22.0 (#2874) --- Cargo.lock | 12 +++++++++--- Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index ad109935d..169df2cff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -360,6 +360,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9475866fec1451be56a3c2400fd081ff546538961565ccb5b7142cbd22bc7a51" + [[package]] name = "bench" version = "0.0.0" @@ -4290,7 +4296,7 @@ dependencies = [ "assert_cmd", "assert_fs", "axoupdater", - "base64 0.21.7", + "base64 0.22.0", "byteorder", "chrono", "clap", @@ -4349,7 +4355,7 @@ name = "uv-auth" version = "0.0.1" dependencies = [ "async-trait", - "base64 0.21.7", + "base64 0.22.0", "clap", "once_cell", "reqwest", @@ -4613,7 +4619,7 @@ name = "uv-git" version = "0.0.1" dependencies = [ "anyhow", - "base64 0.21.7", + "base64 0.22.0", "cache-key", "cargo-util", "fs-err", diff --git a/Cargo.toml b/Cargo.toml index d857df461..b4feb886a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ async_http_range_reader = { version = "0.7.0" } async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "1dcb40cfe1bf5325a6fd4bfcf9894db40241f585", features = ["deflate"] } axoupdater = { version = "0.3.1", default-features = false } backoff = { version = "0.4.0" } -base64 = { version = "0.21.7" } +base64 = { version = "0.22.0" } cachedir = { version = "0.3.1" } cargo-util = { version = "0.2.8" } chrono = { version = "0.4.31" } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 0dfb749a9..3aca2b61b 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -71,7 +71,7 @@ tikv-jemallocator = { version = "0.5.4" } [dev-dependencies] assert_cmd = { version = "2.0.14" } assert_fs = { version = "1.1.0" } -base64 = { version = "0.21.7" } +base64 = { version = "0.22.0" } byteorder = { version = "1.5.0" } filetime = { version = "0.2.23" } indoc = { version = "2.0.4" } From a866cb2f325b2c412578ec9d3d602d5482506372 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:54:16 -0500 Subject: [PATCH 038/110] Update Rust crate insta to v1.38.0 (#2877) --- Cargo.lock | 14 ++------------ 1 file changed, 2 insertions(+), 12 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 169df2cff..f598ffcb9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1782,9 +1782,9 @@ checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" [[package]] name = "insta" -version = "1.36.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e" +checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc" dependencies = [ "console", "lazy_static", @@ -1792,7 +1792,6 @@ dependencies = [ "regex", "serde", "similar", - "yaml-rust", ] [[package]] @@ -5383,15 +5382,6 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "zip" version = "0.6.6" From 61e06bb2c38f12d4ee63c4c5974682e58c99e8fe Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 22:54:40 -0500 Subject: [PATCH 039/110] Update Rust crate rayon to v1.10.0 (#2880) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f598ffcb9..adc0c1ea3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2812,9 +2812,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", From 31813f90c7c90d874c82f0230f0a87ad4cd7f0d0 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 04:35:44 +0000 Subject: [PATCH 040/110] Update pre-commit dependencies (#2893) --- .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 abd3eb80d..21d863457 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.18.2 + rev: v1.20.4 hooks: - id: typos @@ -32,7 +32,7 @@ repos: types_or: [yaml, json5] - repo: https://github.com/astral-sh/ruff-pre-commit - rev: v0.2.1 + rev: v0.3.5 hooks: - id: ruff-format - id: ruff From aa7760534f7b4598b0eb6ab4a7ea121bae9b4057 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 23:41:22 -0500 Subject: [PATCH 041/110] Update dependency ubuntu to v22 (#2897) --- .github/workflows/release.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d965279d2..3fec1e889 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -97,7 +97,7 @@ jobs: needs: - plan - custom-build-binaries - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json @@ -143,7 +143,7 @@ jobs: if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: @@ -198,7 +198,7 @@ jobs: # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }} - runs-on: "ubuntu-20.04" + runs-on: "ubuntu-22.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: From 356a26646c9971c1985f320e608419f59f8880a9 Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Sun, 7 Apr 2024 23:41:49 -0500 Subject: [PATCH 042/110] Update fedora Docker tag to v41 (#2898) --- .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 760b71e31..7a9408fcf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -299,7 +299,7 @@ jobs: needs: build-binary-linux name: "check system | python on fedora" runs-on: ubuntu-latest - container: fedora:39 + container: fedora:41 steps: - uses: actions/checkout@v4 From b181907ad2265b3872f3289bcb31651420ba8350 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Sun, 7 Apr 2024 23:42:19 -0500 Subject: [PATCH 043/110] Fix linehaul tests (#2891) Cleans up the assertions a bit. I looked into snapshot tests per #2564 but it didn't seem worth it for cross-platform tests. Closes #2564 Closes https://github.com/astral-sh/uv/pull/2878 --- Cargo.lock | 11 ------ crates/uv-client/Cargo.toml | 1 - crates/uv-client/tests/user_agent_version.rs | 40 ++++++++------------ 3 files changed, 15 insertions(+), 37 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index adc0c1ea3..1ae4196a2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2344,16 +2344,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "os_info" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" -dependencies = [ - "log", - "winapi", -] - [[package]] name = "overload" version = "0.1.1" @@ -4440,7 +4430,6 @@ dependencies = [ "hyper 0.14.28", "insta", "install-wheel-rs", - "os_info", "pep440_rs", "pep508_rs", "platform-tags", diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 8bcdf8e5d..139c03983 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -57,5 +57,4 @@ webpki-roots = { version = "0.25.4" } anyhow = { workspace = true } hyper = { version = "0.14.28", features = ["server", "http1"] } insta = { version = "1.36.1" } -os_info = { version = "3.7.0", default-features = false } tokio = { workspace = true, features = ["fs", "macros"] } diff --git a/crates/uv-client/tests/user_agent_version.rs b/crates/uv-client/tests/user_agent_version.rs index 6058ec929..c1e59c66e 100644 --- a/crates/uv-client/tests/user_agent_version.rs +++ b/crates/uv-client/tests/user_agent_version.rs @@ -169,26 +169,23 @@ async fn test_user_agent_has_linehaul() -> Result<()> { let system_info = linehaul.system.unwrap(); let impl_info = linehaul.implementation.unwrap(); - assert_eq!(installer_info.name.unwrap(), "uv".to_string()); - assert_eq!(installer_info.version.unwrap(), version()); + assert_eq!(installer_info.name.as_deref(), Some("uv")); + assert_eq!(installer_info.version.as_deref(), Some(version())); - assert_eq!(system_info.name.unwrap(), markers.platform_system); - assert_eq!(system_info.release.unwrap(), markers.platform_release); + assert_eq!(system_info.name, Some(markers.platform_system)); + assert_eq!(system_info.release, Some(markers.platform_release)); + assert_eq!(impl_info.name, Some(markers.platform_python_implementation)); assert_eq!( - impl_info.name.unwrap(), - markers.platform_python_implementation - ); - assert_eq!( - impl_info.version.unwrap(), - markers.python_full_version.version.to_string() + impl_info.version, + Some(markers.python_full_version.version.to_string()) ); assert_eq!( - linehaul.python.unwrap(), - markers.python_full_version.version.to_string() + linehaul.python, + Some(markers.python_full_version.version.to_string()) ); - assert_eq!(linehaul.cpu.unwrap(), markers.platform_machine); + assert_eq!(linehaul.cpu, Some(markers.platform_machine)); assert_eq!(linehaul.openssl_version, None); assert_eq!(linehaul.setuptools_version, None); @@ -197,25 +194,18 @@ async fn test_user_agent_has_linehaul() -> Result<()> { if cfg!(windows) { assert_eq!(linehaul.distro, None); } else if cfg!(target_os = "linux") { - // Using `os_info` to confirm our values are as expected in Linux - let info = os_info::get(); let Some(distro_info) = linehaul.distro else { panic!("got no distro, but expected one in linehaul") }; - assert_eq!(distro_info.id.as_deref(), info.codename()); - if let Some(ref name) = distro_info.name { - assert_eq!(name, &info.os_type().to_string()); - } - if let Some(ref version) = distro_info.version { - assert_eq!(version, &info.version().to_string()); - } + assert_eq!(distro_info.id.as_deref(), Some("jammy")); + assert_eq!(distro_info.name.as_deref(), Some("Ubuntu")); + assert_eq!(distro_info.version.as_deref(), Some("22.04")); assert!(distro_info.libc.is_some()); } else if cfg!(target_os = "macos") { - // We mock the macOS version let distro_info = linehaul.distro.unwrap(); assert_eq!(distro_info.id, None); - assert_eq!(distro_info.name.unwrap(), "macOS"); - assert_eq!(distro_info.version, Some("14.4".to_string())); + assert_eq!(distro_info.name.as_deref(), Some("macOS")); + assert_eq!(distro_info.version.as_deref(), Some("14.4")); assert_eq!(distro_info.libc, None); } From e5ea1785ff0ee57b169377ea7ecf83f0242f9c48 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 8 Apr 2024 01:21:50 -0500 Subject: [PATCH 044/110] Renovate: Group updates to development dependencies (#2888) I don't think we need to audit these individually since they're not user-facing. --- .github/renovate.json5 | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index dd64e6620..10de5afe0 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -29,6 +29,12 @@ matchManagers: ["pre-commit"], description: "Weekly update of pre-commit dependencies", }, + { + groupName: "Rust dev-dependencies", + matchManagers: ["cargo"], + matchDepTypes: ["devDependencies"], + description: "Weekly update of Rust development dependencies", + }, ], vulnerabilityAlerts: { commitMessageSuffix: "", From e3ebd4de10544f12a19148f2e7025cf8d26464fd Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:34:48 -0400 Subject: [PATCH 045/110] Update debian Docker tag to v12 (#2896) --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a9408fcf..f36c651cd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -274,12 +274,12 @@ jobs: needs: build-binary-linux name: "check system | python on debian" runs-on: ubuntu-latest - container: debian:bullseye + container: debian:bookworm steps: - uses: actions/checkout@v4 - name: "Install Python" - run: apt-get update && apt-get install -y python3.9 python3-pip python3.9-venv + run: apt-get update && apt-get install -y python3.11 python3-pip python3.11-venv - name: "Download binary" uses: actions/download-artifact@v4 @@ -290,10 +290,10 @@ jobs: run: chmod +x ./uv - name: "Print Python path" - run: echo $(which python3.9) + run: echo $(which python3.11) - name: "Validate global Python install" - run: python3.9 scripts/check_system_python.py --uv ./uv + run: python3.11 scripts/check_system_python.py --uv ./uv --externally-managed system-test-fedora: needs: build-binary-linux From 47333c985bd143836c2fcceb909da3911c6c5bce Mon Sep 17 00:00:00 2001 From: "renovate[bot]" <29139614+renovate[bot]@users.noreply.github.com> Date: Mon, 8 Apr 2024 09:35:00 -0400 Subject: [PATCH 046/110] Update Rust crate tokio to v1.37.0 (#2886) --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1ae4196a2..0cd3b6ce6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -3862,9 +3862,9 @@ checksum = "b130bd8a58c163224b44e217b4239ca7b927d82bf6cc2fea1fc561d15056e3f7" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", From fb4ba2bbc2ad76f351ef428a2097ede797f64e69 Mon Sep 17 00:00:00 2001 From: konsti Date: Mon, 8 Apr 2024 16:28:56 +0200 Subject: [PATCH 047/110] Speed up cold cache urllib3/boto3/botocore with batched prefetching (#2452) With pubgrub being fast for complex ranges, we can now compute the next n candidates without taking a performance hit. This speeds up cold cache `urllib3<1.25.4` `boto3` from maybe 40s - 50s to ~2s. See docstrings for details on the heuristics. **Before** ![image](https://github.com/astral-sh/uv/assets/6826232/b7b06950-e45b-4c49-b65e-ae19fe9888cc) **After** ![image](https://github.com/astral-sh/uv/assets/6826232/1c749248-850e-49c1-9d57-a7d78f87b3aa) --- We need two parts of the prefetching, first looking for compatible version and then falling back to flat next versions. After we selected a boto3 version, there is only one compatible botocore version remaining, so when won't find other compatible candidates for prefetching. We see this as a pattern where we only prefetch boto3 (stack bars), but not botocore (sequential requests between the stacked bars). ![image](https://github.com/astral-sh/uv/assets/6826232/e5186800-23ac-4ed1-99b9-4d1046fbd03a) The risk is that we're completely wrong with the guess and cause a lot of useless network requests. I think this is acceptable since this mechanism only triggers when we're already on the bad path and we should simply have fetched all versions after some seconds (assuming a fast index like pypi). --- It would be even better if the pubgrub state was copy-on-write so we could simulate more progress than we actually have; currently we're guessing what the next version is which could be completely wrong, but i think this is still a valuable heuristic. Fixes #170. --- .../src/prioritized_distribution.rs | 12 ++ crates/distribution-types/src/resolved.rs | 11 +- .../src/resolver/batch_prefetch.rs | 176 ++++++++++++++++++ crates/uv-resolver/src/resolver/mod.rs | 22 ++- 4 files changed, 217 insertions(+), 4 deletions(-) create mode 100644 crates/uv-resolver/src/resolver/batch_prefetch.rs diff --git a/crates/distribution-types/src/prioritized_distribution.rs b/crates/distribution-types/src/prioritized_distribution.rs index e039eb311..60f98adba 100644 --- a/crates/distribution-types/src/prioritized_distribution.rs +++ b/crates/distribution-types/src/prioritized_distribution.rs @@ -311,6 +311,18 @@ impl<'a> CompatibleDist<'a> { } => ResolvedDistRef::Installable(source_dist), } } + + /// Returns whether the distribution is a source distribution. + /// + /// Avoid building source distributions we don't need. + pub fn prefetchable(&self) -> bool { + match *self { + CompatibleDist::SourceDist(_) => false, + CompatibleDist::InstalledDist(_) + | CompatibleDist::CompatibleWheel(_, _) + | CompatibleDist::IncompatibleWheel { .. } => true, + } + } } impl WheelCompatibility { diff --git a/crates/distribution-types/src/resolved.rs b/crates/distribution-types/src/resolved.rs index 36a269c18..91015e4f3 100644 --- a/crates/distribution-types/src/resolved.rs +++ b/crates/distribution-types/src/resolved.rs @@ -1,4 +1,4 @@ -use std::fmt::Display; +use std::fmt::{Display, Formatter}; use pep508_rs::PackageName; @@ -42,6 +42,15 @@ impl ResolvedDistRef<'_> { } } +impl Display for ResolvedDistRef<'_> { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Installable(dist) => Display::fmt(dist, f), + Self::Installed(dist) => Display::fmt(dist, f), + } + } +} + impl Name for ResolvedDistRef<'_> { fn name(&self) -> &PackageName { match self { diff --git a/crates/uv-resolver/src/resolver/batch_prefetch.rs b/crates/uv-resolver/src/resolver/batch_prefetch.rs new file mode 100644 index 000000000..42f8e6754 --- /dev/null +++ b/crates/uv-resolver/src/resolver/batch_prefetch.rs @@ -0,0 +1,176 @@ +use std::cmp::min; + +use pubgrub::range::Range; +use rustc_hash::FxHashMap; +use tokio::sync::mpsc::Sender; +use tracing::{debug, trace}; + +use distribution_types::{DistributionMetadata, ResolvedDistRef}; +use pep440_rs::Version; + +use crate::candidate_selector::{CandidateDist, CandidateSelector}; +use crate::pubgrub::PubGrubPackage; +use crate::resolver::Request; +use crate::{InMemoryIndex, ResolveError, VersionsResponse}; + +enum BatchPrefetchStrategy { + /// Go through the next versions assuming the existing selection and its constraints + /// remain. + Compatible { + compatible: Range, + previous: Version, + }, + /// We encounter cases (botocore) where the above doesn't work: Say we previously selected + /// a==x.y.z, which depends on b==x.y.z. a==x.y.z is incompatible, but we don't know that + /// yet. We just selected b==x.y.z and want to prefetch, since for all versions of a we try, + /// we have to wait for the matching version of b. The exiting range gives us only one version + /// of b, so the compatible strategy doesn't prefetch any version. Instead, we try the next + /// heuristic where the next version of b will be x.y.(z-1) and so forth. + InOrder { previous: Version }, +} + +/// Prefetch a large number of versions if we already unsuccessfully tried many versions. +/// +/// This is an optimization specifically targeted at cold cache urllib3/boto3/botocore, where we +/// have to fetch the metadata for a lot of versions. +/// +/// Note that these all heuristics that could totally prefetch lots of irrelevant versions. +#[derive(Default)] +pub(crate) struct BatchPrefetcher { + tried_versions: FxHashMap, + last_prefetch: FxHashMap, +} + +impl BatchPrefetcher { + /// Prefetch a large number of versions if we already unsuccessfully tried many versions. + pub(crate) async fn prefetch_batches( + &mut self, + next: &PubGrubPackage, + version: &Version, + current_range: &Range, + request_sink: &Sender, + index: &InMemoryIndex, + selector: &CandidateSelector, + ) -> anyhow::Result<(), ResolveError> { + let PubGrubPackage::Package(package_name, _, _) = &next else { + return Ok(()); + }; + + let (num_tried, do_prefetch) = self.should_prefetch(next); + if !do_prefetch { + return Ok(()); + } + let total_prefetch = min(num_tried, 50); + + // This is immediate, we already fetched the version map. + let versions_response = index + .packages + .wait(package_name) + .await + .ok_or(ResolveError::Unregistered)?; + + let VersionsResponse::Found(ref version_map) = *versions_response else { + return Ok(()); + }; + + let mut phase = BatchPrefetchStrategy::Compatible { + compatible: current_range.clone(), + previous: version.clone(), + }; + let mut prefetch_count = 0; + for _ in 0..total_prefetch { + let candidate = match phase { + BatchPrefetchStrategy::Compatible { + compatible, + previous, + } => { + if let Some(candidate) = + selector.select_no_preference(package_name, &compatible, version_map) + { + let compatible = compatible.intersection( + &Range::singleton(candidate.version().clone()).complement(), + ); + phase = BatchPrefetchStrategy::Compatible { + compatible, + previous: candidate.version().clone(), + }; + candidate + } else { + // We exhausted the compatible version, switch to ignoring the existing + // constraints on the package and instead going through versions in order. + phase = BatchPrefetchStrategy::InOrder { previous }; + continue; + } + } + BatchPrefetchStrategy::InOrder { previous } => { + let range = if selector.use_highest_version(package_name) { + Range::strictly_lower_than(previous) + } else { + Range::strictly_higher_than(previous) + }; + if let Some(candidate) = + selector.select_no_preference(package_name, &range, version_map) + { + phase = BatchPrefetchStrategy::InOrder { + previous: candidate.version().clone(), + }; + candidate + } else { + // Both strategies exhausted their candidates. + break; + } + } + }; + + let CandidateDist::Compatible(dist) = candidate.dist() else { + continue; + }; + // Avoid building a lot of source distributions. + if !dist.prefetchable() { + continue; + } + let dist = dist.for_resolution(); + + // Emit a request to fetch the metadata for this version. + trace!( + "Prefetching {prefetch_count} ({}) {}", + match phase { + BatchPrefetchStrategy::Compatible { .. } => "compatible", + BatchPrefetchStrategy::InOrder { .. } => "in order", + }, + dist + ); + prefetch_count += 1; + if index.distributions.register(candidate.package_id()) { + let request = match dist { + ResolvedDistRef::Installable(dist) => Request::Dist(dist.clone()), + ResolvedDistRef::Installed(dist) => Request::Installed(dist.clone()), + }; + request_sink.send(request).await?; + } + } + + debug!("Prefetching {prefetch_count} {package_name} versions"); + + self.last_prefetch.insert(next.clone(), num_tried); + Ok(()) + } + + /// Each time we tried a version for a package, we register that here. + pub(crate) fn version_tried(&mut self, package: PubGrubPackage) { + *self.tried_versions.entry(package).or_default() += 1; + } + + /// After 5, 10, 20, 40 tried versions, prefetch that many versions to start early but not + /// too aggressive. Later we schedule the prefetch of 50 versions every 20 versions, this gives + /// us a good buffer until we see prefetch again and is high enough to saturate the task pool. + fn should_prefetch(&self, next: &PubGrubPackage) -> (usize, bool) { + let num_tried = self.tried_versions.get(next).copied().unwrap_or_default(); + let previous_prefetch = self.last_prefetch.get(next).copied().unwrap_or_default(); + let do_prefetch = (num_tried >= 5 && previous_prefetch < 5) + || (num_tried >= 10 && previous_prefetch < 10) + || (num_tried >= 20 && previous_prefetch < 20) + || (num_tried >= 20 && num_tried - previous_prefetch >= 20); + (num_tried, do_prefetch) + } +} diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 7aaed6ce7..6fb72512b 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -44,6 +44,7 @@ use crate::pubgrub::{ }; use crate::python_requirement::PythonRequirement; use crate::resolution::ResolutionGraph; +use crate::resolver::batch_prefetch::BatchPrefetcher; pub use crate::resolver::index::InMemoryIndex; pub use crate::resolver::provider::{ DefaultResolverProvider, MetadataResponse, PackageVersionsResult, ResolverProvider, @@ -54,6 +55,7 @@ pub use crate::resolver::reporter::{BuildId, Reporter}; use crate::yanks::AllowedYanks; use crate::{DependencyMode, Exclusions, Options}; +mod batch_prefetch; mod index; mod locals; mod provider; @@ -217,14 +219,14 @@ impl< pub async fn resolve(self) -> Result { // A channel to fetch package metadata (e.g., given `flask`, fetch all versions) and version // metadata (e.g., given `flask==1.0.0`, fetch the metadata for that version). - // Channel size is set to the same size as the task buffer for simplicity. - let (request_sink, request_stream) = tokio::sync::mpsc::channel(50); + // Channel size is set large to accommodate batch prefetching. + let (request_sink, request_stream) = tokio::sync::mpsc::channel(300); // Run the fetcher. let requests_fut = self.fetch(request_stream).fuse(); // Run the solver. - let resolve_fut = self.solve(request_sink).fuse(); + let resolve_fut = self.solve(request_sink).boxed().fuse(); // Wait for both to complete. match tokio::try_join!(requests_fut, resolve_fut) { @@ -260,6 +262,7 @@ impl< request_sink: tokio::sync::mpsc::Sender, ) -> Result { let root = PubGrubPackage::Root(self.project.clone()); + let mut prefetcher = BatchPrefetcher::default(); // Keep track of the packages for which we've requested metadata. let mut pins = FilePins::default(); @@ -308,6 +311,8 @@ impl< }; next = highest_priority_pkg; + prefetcher.version_tried(next.clone()); + let term_intersection = state .partial_solution .term_intersection_for_package(&next) @@ -415,6 +420,17 @@ impl< } }; + prefetcher + .prefetch_batches( + &next, + &version, + term_intersection.unwrap_positive(), + &request_sink, + self.index, + &self.selector, + ) + .await?; + self.on_progress(&next, &version); if added_dependencies From f11a5e2208a6c6f0c4211c56114cf7b4e845da55 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 10:40:17 -0400 Subject: [PATCH 048/110] DRY up local wheel path in distribution database (#2901) --- .../src/distribution_database.rs | 93 +++++++------------ scripts/requirements/all-kinds.in | 1 - 2 files changed, 36 insertions(+), 58 deletions(-) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 3b22dfbaa..ab1475153 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -151,35 +151,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> WheelCache::Url(&url).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); - - // If the file is already unzipped, and the unzipped directory is fresh, - // return it. - match cache_entry.path().canonicalize() { - Ok(archive) => { - if ArchiveTimestamp::up_to_date_with( - path, - ArchiveTarget::Cache(&archive), - ) - .map_err(Error::CacheRead)? - { - return Ok(LocalWheel::Unzipped(UnzippedWheel { - dist: Dist::Built(dist.clone()), - archive, - filename: wheel.filename.clone(), - })); - } - } - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => return Err(Error::CacheRead(err)), - } - - // Otherwise, unzip the file. - return Ok(LocalWheel::Disk(DiskWheel { - dist: Dist::Built(dist.clone()), - path: path.clone(), - target: cache_entry.into_path_buf(), - filename: wheel.filename.clone(), - })); + return Self::load_wheel(path, &wheel.filename, cache_entry, dist); } }; @@ -269,34 +241,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); - - // If the file is already unzipped, and the unzipped directory is fresh, - // return it. - match cache_entry.path().canonicalize() { - Ok(archive) => { - if ArchiveTimestamp::up_to_date_with( - &wheel.path, - ArchiveTarget::Cache(&archive), - ) - .map_err(Error::CacheRead)? - { - return Ok(LocalWheel::Unzipped(UnzippedWheel { - dist: Dist::Built(dist.clone()), - archive, - filename: wheel.filename.clone(), - })); - } - } - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => return Err(Error::CacheRead(err)), - } - - Ok(LocalWheel::Disk(DiskWheel { - dist: Dist::Built(dist.clone()), - path: wheel.path.clone(), - target: cache_entry.into_path_buf(), - filename: wheel.filename.clone(), - })) + Self::load_wheel(&wheel.path, &wheel.filename, cache_entry, dist) } } } @@ -506,6 +451,40 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> Ok(archive) } + /// Load a wheel from a local path. + fn load_wheel( + path: &Path, + filename: &WheelFilename, + wheel_entry: CacheEntry, + dist: &BuiltDist, + ) -> Result { + // If the file is already unzipped, and the unzipped directory is fresh, + // return it. + match wheel_entry.path().canonicalize() { + Ok(archive) => { + if ArchiveTimestamp::up_to_date_with(path, ArchiveTarget::Cache(&archive)) + .map_err(Error::CacheRead)? + { + return Ok(LocalWheel::Unzipped(UnzippedWheel { + dist: Dist::Built(dist.clone()), + archive, + filename: filename.clone(), + })); + } + } + Err(err) if err.kind() == io::ErrorKind::NotFound => {} + Err(err) => return Err(Error::CacheRead(err)), + } + + // Otherwise, return the path to the file. + Ok(LocalWheel::Disk(DiskWheel { + dist: Dist::Built(dist.clone()), + path: path.to_path_buf(), + target: wheel_entry.into_path_buf(), + filename: filename.clone(), + })) + } + /// Returns a GET [`reqwest::Request`] for the given URL. fn request(&self, url: Url) -> Result { self.client diff --git a/scripts/requirements/all-kinds.in b/scripts/requirements/all-kinds.in index 1fe196b8d..4b48f77a3 100644 --- a/scripts/requirements/all-kinds.in +++ b/scripts/requirements/all-kinds.in @@ -10,4 +10,3 @@ django_allauth==0.51.0 werkzeug @ https://files.pythonhosted.org/packages/0d/cc/ff1904eb5eb4b455e442834dabf9427331ac0fa02853bf83db817a7dd53d/werkzeug-3.0.1.tar.gz # git source dist pydantic-extra-types @ git+https://github.com/pydantic/pydantic-extra-types.git - From 10dfd43af97ac5368b6a1c9340441df048d32f68 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 10:45:26 -0400 Subject: [PATCH 049/110] DRY up HTTP request builder in source database (#2902) --- .../src/distribution_database.rs | 7 ++-- crates/uv-distribution/src/source/mod.rs | 41 ++++++++----------- 2 files changed, 21 insertions(+), 27 deletions(-) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index ab1475153..d8c0a0cf4 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -358,6 +358,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .instrument(info_span!("wheel", wheel = %dist)) }; + let req = self.request(url.clone())?; let cache_control = match self.client.connectivity() { Connectivity::Online => CacheControl::from( self.build_context @@ -367,11 +368,10 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> ), Connectivity::Offline => CacheControl::AllowStale, }; - let archive = self .client .cached_client() - .get_serde(self.request(url)?, &http_entry, cache_control, download) + .get_serde(req, &http_entry, cache_control, download) .await .map_err(|err| match err { CachedClientError::Callback(err) => err, @@ -428,6 +428,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .instrument(info_span!("wheel", wheel = %dist)) }; + let req = self.request(url.clone())?; let cache_control = match self.client.connectivity() { Connectivity::Online => CacheControl::from( self.build_context @@ -441,7 +442,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let archive = self .client .cached_client() - .get_serde(self.request(url)?, &http_entry, cache_control, download) + .get_serde(req, &http_entry, cache_control, download) .await .map_err(|err| match err { CachedClientError::Callback(err) => err, diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 4be6443d4..f968b94ea 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -356,18 +356,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .boxed() .instrument(info_span!("download", source_dist = %source)) }; - let req = self - .client - .uncached_client() - .get(url.clone()) - .header( - // `reqwest` defaults to accepting compressed responses. - // Specify identity encoding to get consistent .whl downloading - // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 - "accept-encoding", - reqwest::header::HeaderValue::from_static("identity"), - ) - .build()?; + let req = self.request(url.clone())?; let manifest = self .client .cached_client() @@ -460,18 +449,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .boxed() .instrument(info_span!("download", source_dist = %source)) }; - let req = self - .client - .uncached_client() - .get(url.clone()) - .header( - // `reqwest` defaults to accepting compressed responses. - // Specify identity encoding to get consistent .whl downloading - // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 - "accept-encoding", - reqwest::header::HeaderValue::from_static("identity"), - ) - .build()?; + let req = self.request(url.clone())?; let manifest = self .client .cached_client() @@ -1103,6 +1081,21 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Finished building (editable): {dist}"); Ok((dist, disk_filename, filename, metadata)) } + + /// Returns a GET [`reqwest::Request`] for the given URL. + fn request(&self, url: Url) -> Result { + self.client + .uncached_client() + .get(url) + .header( + // `reqwest` defaults to accepting compressed responses. + // Specify identity encoding to get consistent .whl downloading + // behavior from servers. ref: https://github.com/pypa/pip/pull/1688 + "accept-encoding", + reqwest::header::HeaderValue::from_static("identity"), + ) + .build() + } } #[derive(Debug)] From f1630a70f5375009c59cafb72db95cfbe00c2f5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C5=82awomir=20Ehlert?= Date: Mon, 8 Apr 2024 19:18:53 +0200 Subject: [PATCH 050/110] Suppress `MultipleHandlers` from Ctrl-C in confirm (#2903) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## Summary Fixes #2900 ## Test Plan Tried reproducing the steps described in #2900, but with `cargo run -- pip ...` and it didn't crash 😄. --- crates/uv-requirements/src/confirm.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/crates/uv-requirements/src/confirm.rs b/crates/uv-requirements/src/confirm.rs index 3b111d331..7eb7d8b59 100644 --- a/crates/uv-requirements/src/confirm.rs +++ b/crates/uv-requirements/src/confirm.rs @@ -6,7 +6,8 @@ use console::{style, Key, Term}; /// This is a slimmed-down version of `dialoguer::Confirm`, with the post-confirmation report /// enabled. pub(crate) fn confirm(message: &str, term: &Term, default: bool) -> Result { - ctrlc::set_handler(move || { + // Set the Ctrl-C handler to exit the process. + let result = ctrlc::set_handler(move || { let term = Term::stderr(); term.show_cursor().ok(); term.flush().ok(); @@ -17,7 +18,16 @@ pub(crate) fn confirm(message: &str, term: &Term, default: bool) -> Result } else { 130 }); - })?; + }); + + match result { + Ok(()) => {} + Err(ctrlc::Error::MultipleHandlers) => { + // If multiple handlers were set, we assume that the existing handler is our + // confirmation handler, and continue. + } + Err(e) => return Err(e.into()), + } let prompt = format!( "{} {} {} {} {}", From 1daa35176fd03bcc2a7d8440da4da706d5adae2d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 14:07:17 -0400 Subject: [PATCH 051/110] Always return unzipped wheels from the distribution database (#2905) ## Summary In all cases, we unzip these immediately after returning. By moving the unzipping into the database, we can remove a bunch of code (coming in a separate PR), and pave the way for hash-checking, since hash generation will _also_ happen in the database, and splitting the caching layers across the database and the unzipper creates complications. Closes #2863. --- .../src/distribution_database.rs | 88 +++++++++++++------ 1 file changed, 63 insertions(+), 25 deletions(-) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index d8c0a0cf4..79ad0acf5 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -3,6 +3,7 @@ use std::path::{Path, PathBuf}; use std::sync::Arc; use futures::{FutureExt, TryStreamExt}; +use tempfile::TempDir; use tokio::io::AsyncSeekExt; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{info_span, instrument, warn, Instrument}; @@ -18,9 +19,9 @@ use uv_cache::{ArchiveTarget, ArchiveTimestamp, CacheBucket, CacheEntry, WheelCa use uv_client::{CacheControl, CachedClientError, Connectivity, RegistryClient}; use uv_types::{BuildContext, NoBinary, NoBuild}; -use crate::download::{BuiltWheel, UnzippedWheel}; +use crate::download::UnzippedWheel; use crate::locks::Locks; -use crate::{DiskWheel, Error, LocalWheel, Reporter, SourceDistributionBuilder}; +use crate::{Error, LocalWheel, Reporter, SourceDistributionBuilder}; /// A cached high-level interface to convert distributions (a requirement resolved to a location) /// to a wheel or wheel metadata. @@ -110,18 +111,23 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> editable: &LocalEditable, editable_wheel_dir: &Path, ) -> Result<(LocalWheel, Metadata23), Error> { + // Build the wheel. let (dist, disk_filename, filename, metadata) = self .builder .build_editable(editable, editable_wheel_dir) .await?; - let built_wheel = BuiltWheel { + // Unzip. + let path = editable_wheel_dir.join(&disk_filename); + let target = editable_wheel_dir.join(cache_key::digest(&editable.path)); + let archive = self.unzip_wheel(&path, &target).await?; + let wheel = LocalWheel::Unzipped(UnzippedWheel { dist, filename, - path: editable_wheel_dir.join(disk_filename), - target: editable_wheel_dir.join(cache_key::digest(&editable.path)), - }; - Ok((LocalWheel::Built(built_wheel), metadata)) + archive, + }); + + Ok((wheel, metadata)) } /// Fetch a wheel from the cache or download it from the index. @@ -151,7 +157,9 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> WheelCache::Url(&url).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); - return Self::load_wheel(path, &wheel.filename, cache_entry, dist); + return self + .load_wheel(path, &wheel.filename, cache_entry, dist) + .await; } }; @@ -241,7 +249,8 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); - Self::load_wheel(&wheel.path, &wheel.filename, cache_entry, dist) + self.load_wheel(&wheel.path, &wheel.filename, cache_entry, dist) + .await } } } @@ -261,21 +270,25 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> // If the wheel was unzipped previously, respect it. Source distributions are // cached under a unique build ID, so unzipped directories are never stale. match built_wheel.target.canonicalize() { - Ok(archive) => Ok(LocalWheel::Unzipped(UnzippedWheel { - dist: Dist::Source(dist.clone()), - archive, - filename: built_wheel.filename, - })), - Err(err) if err.kind() == io::ErrorKind::NotFound => { - Ok(LocalWheel::Built(BuiltWheel { + Ok(archive) => { + return Ok(LocalWheel::Unzipped(UnzippedWheel { dist: Dist::Source(dist.clone()), - path: built_wheel.path, - target: built_wheel.target, + archive, filename: built_wheel.filename, - })) + })); } - Err(err) => Err(Error::CacheRead(err)), + Err(err) if err.kind() == io::ErrorKind::NotFound => {} + Err(err) => return Err(Error::CacheRead(err)), } + + // Otherwise, unzip the wheel. + Ok(LocalWheel::Unzipped(UnzippedWheel { + dist: Dist::Source(dist.clone()), + archive: self + .unzip_wheel(&built_wheel.path, &built_wheel.target) + .await?, + filename: built_wheel.filename, + })) } /// Fetch the wheel metadata from the index, or from the cache if possible. @@ -453,7 +466,8 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> } /// Load a wheel from a local path. - fn load_wheel( + async fn load_wheel( + &self, path: &Path, filename: &WheelFilename, wheel_entry: CacheEntry, @@ -477,15 +491,39 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> Err(err) => return Err(Error::CacheRead(err)), } - // Otherwise, return the path to the file. - Ok(LocalWheel::Disk(DiskWheel { + // Otherwise, unzip the wheel. + Ok(LocalWheel::Unzipped(UnzippedWheel { dist: Dist::Built(dist.clone()), - path: path.to_path_buf(), - target: wheel_entry.into_path_buf(), + archive: self.unzip_wheel(path, wheel_entry.path()).await?, filename: filename.clone(), })) } + /// Unzip a wheel into the cache, returning the path to the unzipped directory. + async fn unzip_wheel(&self, path: &Path, target: &Path) -> Result { + let temp_dir = tokio::task::spawn_blocking({ + let path = path.to_owned(); + let root = self.build_context.cache().root().to_path_buf(); + move || -> Result { + // Unzip the wheel into a temporary directory. + let temp_dir = tempfile::tempdir_in(root)?; + uv_extract::unzip(fs_err::File::open(path)?, temp_dir.path())?; + Ok(temp_dir) + } + }) + .await??; + + // Persist the temporary directory to the directory store. + let archive = self + .build_context + .cache() + .persist(temp_dir.into_path(), target) + .await + .map_err(Error::CacheWrite)?; + + Ok(archive) + } + /// Returns a GET [`reqwest::Request`] for the given URL. fn request(&self, url: Url) -> Result { self.client From 31a67f539f860168b133fdba0889a339c1f7bd16 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 14:15:20 -0400 Subject: [PATCH 052/110] Remove unused local wheel types (#2906) ## Summary No behavior changes. Just removing unused code. --- .../src/distribution_database.rs | 37 +++--- crates/uv-distribution/src/download.rs | 113 ++---------------- crates/uv-distribution/src/lib.rs | 4 +- crates/uv-distribution/src/unzip.rs | 36 ------ crates/uv-installer/src/downloader.rs | 42 +------ 5 files changed, 34 insertions(+), 198 deletions(-) delete mode 100644 crates/uv-distribution/src/unzip.rs diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 79ad0acf5..e5c75187a 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -19,7 +19,6 @@ use uv_cache::{ArchiveTarget, ArchiveTimestamp, CacheBucket, CacheEntry, WheelCa use uv_client::{CacheControl, CachedClientError, Connectivity, RegistryClient}; use uv_types::{BuildContext, NoBinary, NoBuild}; -use crate::download::UnzippedWheel; use crate::locks::Locks; use crate::{Error, LocalWheel, Reporter, SourceDistributionBuilder}; @@ -121,11 +120,11 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let path = editable_wheel_dir.join(&disk_filename); let target = editable_wheel_dir.join(cache_key::digest(&editable.path)); let archive = self.unzip_wheel(&path, &target).await?; - let wheel = LocalWheel::Unzipped(UnzippedWheel { + let wheel = LocalWheel { dist, filename, archive, - }); + }; Ok((wheel, metadata)) } @@ -175,11 +174,11 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .stream_wheel(url.clone(), &wheel.filename, &wheel_entry, dist) .await { - Ok(archive) => Ok(LocalWheel::Unzipped(UnzippedWheel { + Ok(archive) => Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive, filename: wheel.filename.clone(), - })), + }), Err(Error::Extract(err)) if err.is_http_streaming_unsupported() => { warn!( "Streaming unsupported for {dist}; downloading wheel to disk ({err})" @@ -190,11 +189,11 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let archive = self .download_wheel(url, &wheel.filename, &wheel_entry, dist) .await?; - Ok(LocalWheel::Unzipped(UnzippedWheel { + Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive, filename: wheel.filename.clone(), - })) + }) } Err(err) => Err(err), } @@ -213,11 +212,11 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .stream_wheel(wheel.url.raw().clone(), &wheel.filename, &wheel_entry, dist) .await { - Ok(archive) => Ok(LocalWheel::Unzipped(UnzippedWheel { + Ok(archive) => Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive, filename: wheel.filename.clone(), - })), + }), Err(Error::Client(err)) if err.is_http_streaming_unsupported() => { warn!( "Streaming unsupported for {dist}; downloading wheel to disk ({err})" @@ -233,11 +232,11 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> dist, ) .await?; - Ok(LocalWheel::Unzipped(UnzippedWheel { + Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive, filename: wheel.filename.clone(), - })) + }) } Err(err) => Err(err), } @@ -271,24 +270,24 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> // cached under a unique build ID, so unzipped directories are never stale. match built_wheel.target.canonicalize() { Ok(archive) => { - return Ok(LocalWheel::Unzipped(UnzippedWheel { + return Ok(LocalWheel { dist: Dist::Source(dist.clone()), archive, filename: built_wheel.filename, - })); + }); } Err(err) if err.kind() == io::ErrorKind::NotFound => {} Err(err) => return Err(Error::CacheRead(err)), } // Otherwise, unzip the wheel. - Ok(LocalWheel::Unzipped(UnzippedWheel { + Ok(LocalWheel { dist: Dist::Source(dist.clone()), archive: self .unzip_wheel(&built_wheel.path, &built_wheel.target) .await?, filename: built_wheel.filename, - })) + }) } /// Fetch the wheel metadata from the index, or from the cache if possible. @@ -480,11 +479,11 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> if ArchiveTimestamp::up_to_date_with(path, ArchiveTarget::Cache(&archive)) .map_err(Error::CacheRead)? { - return Ok(LocalWheel::Unzipped(UnzippedWheel { + return Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive, filename: filename.clone(), - })); + }); } } Err(err) if err.kind() == io::ErrorKind::NotFound => {} @@ -492,11 +491,11 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> } // Otherwise, unzip the wheel. - Ok(LocalWheel::Unzipped(UnzippedWheel { + Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: self.unzip_wheel(path, wheel_entry.path()).await?, filename: filename.clone(), - })) + }) } /// Unzip a wheel into the cache, returning the path to the unzipped directory. diff --git a/crates/uv-distribution/src/download.rs b/crates/uv-distribution/src/download.rs index 7ee3726b5..b7123ff7c 100644 --- a/crates/uv-distribution/src/download.rs +++ b/crates/uv-distribution/src/download.rs @@ -6,9 +6,9 @@ use pypi_types::Metadata23; use crate::Error; -/// A wheel that's been unzipped while downloading +/// A locally available wheel. #[derive(Debug, Clone)] -pub struct UnzippedWheel { +pub struct LocalWheel { /// The remote distribution from which this wheel was downloaded. pub(crate) dist: Dist, /// The parsed filename. @@ -18,113 +18,32 @@ pub struct UnzippedWheel { pub(crate) archive: PathBuf, } -/// A downloaded wheel that's stored on-disk. -#[derive(Debug, Clone)] -pub struct DiskWheel { - /// The remote distribution from which this wheel was downloaded. - pub(crate) dist: Dist, - /// The parsed filename. - pub(crate) filename: WheelFilename, - /// The path to the downloaded wheel. - pub(crate) path: PathBuf, - /// The expected path to the downloaded wheel's entry in the cache. - /// Typically, a symlink within the wheels or built wheels bucket. - pub(crate) target: PathBuf, -} - -/// A wheel built from a source distribution that's stored on-disk. -#[derive(Debug, Clone)] -pub struct BuiltWheel { - /// The remote source distribution from which this wheel was built. - pub(crate) dist: Dist, - /// The parsed filename. - pub(crate) filename: WheelFilename, - /// The path to the built wheel. - pub(crate) path: PathBuf, - /// The expected path to the downloaded wheel's entry in the cache. - /// Typically, a symlink within the wheels or built wheels bucket. - pub(crate) target: PathBuf, -} - -/// A downloaded or built wheel. -#[derive(Debug, Clone)] -pub enum LocalWheel { - Unzipped(UnzippedWheel), - Disk(DiskWheel), - Built(BuiltWheel), -} - impl LocalWheel { /// Return the path to the downloaded wheel's entry in the cache. pub fn target(&self) -> &Path { - match self { - Self::Unzipped(wheel) => &wheel.archive, - Self::Disk(wheel) => &wheel.target, - Self::Built(wheel) => &wheel.target, - } + &self.archive } /// Return the [`Dist`] from which this wheel was downloaded. pub fn remote(&self) -> &Dist { - match self { - Self::Unzipped(wheel) => wheel.remote(), - Self::Disk(wheel) => wheel.remote(), - Self::Built(wheel) => wheel.remote(), - } + &self.dist } /// Return the [`WheelFilename`] of this wheel. pub fn filename(&self) -> &WheelFilename { - match self { - Self::Unzipped(wheel) => &wheel.filename, - Self::Disk(wheel) => &wheel.filename, - Self::Built(wheel) => &wheel.filename, - } - } - - /// Convert a [`LocalWheel`] into a [`CachedDist`]. - pub fn into_cached_dist(self, archive: PathBuf) -> CachedDist { - match self { - Self::Unzipped(wheel) => CachedDist::from_remote(wheel.dist, wheel.filename, archive), - Self::Disk(wheel) => CachedDist::from_remote(wheel.dist, wheel.filename, archive), - Self::Built(wheel) => CachedDist::from_remote(wheel.dist, wheel.filename, archive), - } + &self.filename } /// Read the [`Metadata23`] from a wheel. pub fn metadata(&self) -> Result { - match self { - Self::Unzipped(wheel) => read_flat_wheel_metadata(&wheel.filename, &wheel.archive), - Self::Disk(wheel) => read_built_wheel_metadata(&wheel.filename, &wheel.path), - Self::Built(wheel) => read_built_wheel_metadata(&wheel.filename, &wheel.path), - } + read_flat_wheel_metadata(&self.filename, &self.archive) } } -impl UnzippedWheel { - /// Return the [`Dist`] from which this wheel was downloaded. - pub fn remote(&self) -> &Dist { - &self.dist - } - - /// Convert an [`UnzippedWheel`] into a [`CachedDist`]. - pub fn into_cached_dist(self) -> CachedDist { - CachedDist::from_remote(self.dist, self.filename, self.archive) - } -} - -impl DiskWheel { - /// Return the [`Dist`] from which this wheel was downloaded. - pub fn remote(&self) -> &Dist { - &self.dist - } -} - -impl BuiltWheel { - /// Return the [`Dist`] from which this source distribution that this wheel was built from was - /// downloaded. - pub fn remote(&self) -> &Dist { - &self.dist +/// Convert a [`LocalWheel`] into a [`CachedDist`]. +impl From for CachedDist { + fn from(wheel: LocalWheel) -> CachedDist { + CachedDist::from_remote(wheel.dist, wheel.filename, wheel.archive) } } @@ -134,18 +53,6 @@ impl std::fmt::Display for LocalWheel { } } -/// Read the [`Metadata23`] from a built wheel. -fn read_built_wheel_metadata( - filename: &WheelFilename, - wheel: impl AsRef, -) -> Result { - let file = fs_err::File::open(wheel.as_ref()).map_err(Error::CacheRead)?; - let reader = std::io::BufReader::new(file); - let mut archive = zip::ZipArchive::new(reader)?; - let metadata = install_wheel_rs::metadata::read_archive_metadata(filename, &mut archive)?; - Ok(Metadata23::parse_metadata(&metadata)?) -} - /// Read the [`Metadata23`] from an unzipped wheel. fn read_flat_wheel_metadata( filename: &WheelFilename, diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index 5c04678c4..f74b0fc9d 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -1,11 +1,10 @@ pub use distribution_database::DistributionDatabase; -pub use download::{BuiltWheel, DiskWheel, LocalWheel}; +pub use download::LocalWheel; pub use error::Error; pub use git::{is_same_reference, to_precise}; pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use reporter::Reporter; pub use source::SourceDistributionBuilder; -pub use unzip::Unzip; mod distribution_database; mod download; @@ -15,4 +14,3 @@ mod index; mod locks; mod reporter; mod source; -mod unzip; diff --git a/crates/uv-distribution/src/unzip.rs b/crates/uv-distribution/src/unzip.rs deleted file mode 100644 index 92f5b4867..000000000 --- a/crates/uv-distribution/src/unzip.rs +++ /dev/null @@ -1,36 +0,0 @@ -use std::path::Path; - -use tracing::instrument; - -use uv_extract::Error; - -use crate::download::BuiltWheel; -use crate::{DiskWheel, LocalWheel}; - -pub trait Unzip { - /// Unzip a wheel into the target directory. - fn unzip(&self, target: &Path) -> Result<(), Error>; -} - -impl Unzip for DiskWheel { - fn unzip(&self, target: &Path) -> Result<(), Error> { - uv_extract::unzip(fs_err::File::open(&self.path)?, target) - } -} - -impl Unzip for BuiltWheel { - fn unzip(&self, target: &Path) -> Result<(), Error> { - uv_extract::unzip(fs_err::File::open(&self.path)?, target) - } -} - -impl Unzip for LocalWheel { - #[instrument(skip_all, fields(filename=self.filename().to_string()))] - fn unzip(&self, target: &Path) -> Result<(), Error> { - match self { - Self::Unzipped(_) => Ok(()), - Self::Disk(wheel) => wheel.unzip(target), - Self::Built(wheel) => wheel.unzip(target), - } - } -} diff --git a/crates/uv-installer/src/downloader.rs b/crates/uv-installer/src/downloader.rs index e84405be1..bba3c2fa7 100644 --- a/crates/uv-installer/src/downloader.rs +++ b/crates/uv-installer/src/downloader.rs @@ -3,7 +3,6 @@ use std::path::Path; use std::sync::Arc; use futures::{FutureExt, Stream, StreamExt, TryFutureExt, TryStreamExt}; -use tempfile::TempDir; use tokio::task::JoinError; use tracing::instrument; use url::Url; @@ -14,7 +13,7 @@ use distribution_types::{ use platform_tags::Tags; use uv_cache::Cache; use uv_client::RegistryClient; -use uv_distribution::{DistributionDatabase, LocalWheel, Unzip}; +use uv_distribution::DistributionDatabase; use uv_types::{BuildContext, InFlight}; use crate::editable::BuiltEditable; @@ -133,7 +132,7 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { .build_wheel_editable(&editable, editable_wheel_dir) .await .map_err(Error::Editable)?; - let cached_dist = self.unzip_wheel(local_wheel).await?; + let cached_dist = CachedDist::from(local_wheel); if let Some(task_id) = task_id { if let Some(reporter) = &self.reporter { reporter.on_editable_build_complete(&editable, task_id); @@ -166,13 +165,13 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { pub async fn get_wheel(&self, dist: Dist, in_flight: &InFlight) -> Result { let id = dist.distribution_id(); if in_flight.downloads.register(id.clone()) { - let download: LocalWheel = self + let result = self .database .get_or_build_wheel(&dist, self.tags) .boxed() .map_err(|err| Error::Fetch(dist.clone(), err)) - .await?; - let result = self.unzip_wheel(download).await; + .await + .map(CachedDist::from); match result { Ok(cached) => { in_flight.downloads.done(id, Ok(cached.clone())); @@ -196,37 +195,6 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { } } } - - /// Unzip a locally-available wheel into the cache. - async fn unzip_wheel(&self, download: LocalWheel) -> Result { - // Just an optimization: Avoid spawning a blocking task if there is no work to be done. - if let LocalWheel::Unzipped(download) = download { - return Ok(download.into_cached_dist()); - } - - // Unzip the wheel. - let temp_dir = tokio::task::spawn_blocking({ - let download = download.clone(); - let cache = self.cache.clone(); - move || -> Result { - // Unzip the wheel into a temporary directory. - let temp_dir = tempfile::tempdir_in(cache.root())?; - download.unzip(temp_dir.path())?; - Ok(temp_dir) - } - }) - .await? - .map_err(|err| Error::Unzip(download.remote().clone(), err))?; - - // Persist the temporary directory to the directory store. - let archive = self - .cache - .persist(temp_dir.into_path(), download.target()) - .map_err(Error::CacheWrite) - .await?; - - Ok(download.into_cached_dist(archive)) - } } pub trait Reporter: Send + Sync { From 134810c5478d0623db663dcd47f6ff89435f129c Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 14:58:33 -0400 Subject: [PATCH 053/110] Respect cached local `--find-links` in install plan (#2907) ## Summary I think this is kind of just an oversight. If a wheel is available via `--find-links`, and the index is "local", we never find it in the cache. ## Test Plan `cargo test` --- .../src/distribution_database.rs | 3 +- .../src/index/registry_wheel_index.rs | 5 +- crates/uv/tests/pip_sync.rs | 48 +++++++++++++++++++ 3 files changed, 53 insertions(+), 3 deletions(-) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index e5c75187a..5c63f5428 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -150,10 +150,9 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> Url::parse(url).map_err(|err| Error::Url(url.clone(), err))? } FileLocation::Path(path) => { - let url = Url::from_file_path(path).expect("path is absolute"); let cache_entry = self.build_context.cache().entry( CacheBucket::Wheels, - WheelCache::Url(&url).wheel_dir(wheel.name().as_ref()), + WheelCache::Index(&wheel.index).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); return self diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 1aa5ab017..8342cdb8a 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -83,7 +83,10 @@ impl<'a> RegistryWheelIndex<'a> { let flat_index_urls: Vec = index_locations .flat_index() .filter_map(|flat_index| match flat_index { - FlatIndexLocation::Path(_) => None, + FlatIndexLocation::Path(path) => { + let path = fs_err::canonicalize(path).ok()?; + Some(IndexUrl::Url(VerbatimUrl::from_path(path))) + } FlatIndexLocation::Url(url) => { Some(IndexUrl::Url(VerbatimUrl::unknown(url.clone()))) } diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 3f15bb13d..79088af57 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -2575,6 +2575,54 @@ fn find_links_offline_no_match() -> Result<()> { Ok(()) } +/// Sync using `--find-links` with a local directory. Ensure that cached wheels are reused. +#[test] +fn find_links_cache() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc! {r" + tqdm + "})?; + + // Install `tqdm`. + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + tqdm==1000.0.0 + "### + ); + + // Reinstall `tqdm` with `--reinstall`. Ensure that the wheel is reused. + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - tqdm==1000.0.0 + + tqdm==1000.0.0 + "### + ); + + Ok(()) +} + /// Install without network access via the `--offline` flag. #[test] fn offline() -> Result<()> { From c46772eec531a781d1c4b9d6a5b14a20dd9e6861 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 15:32:59 -0400 Subject: [PATCH 054/110] Add a layer of indirection to the local path-based wheel cache (#2909) ## Summary Right now, the path-based wheel cache just looks at the symlink to the archives directory, checks the timestamp on it, and continues with that symlink as long as the timestamp is up-to-date. The HTTP-based wheel meanwhile, uses an intermediary `.http` file, which includes the HTTP caching information. The `.http` file's payload is just a path pointing to an entry in the archives directory. This PR modifies the path-based codepaths to use a similar cache file, which stores a timestamp along with a path to the archives directory. The main advantage here is that we can add other data to this cache file (namely, hashes in the future). ## Test Plan Beyond existing tests, I also verified that this doesn't require a version bump: ``` git checkout main cargo run pip install ~/Downloads/zeal-0.0.1-py3-none-any.whl --cache-dir baz --reinstall git checkout charlie/manifest cargo run pip install ~/Downloads/zeal-0.0.1-py3-none-any.whl --cache-dir baz --reinstall cargo run pip install ~/Downloads/zeal-0.0.1-py3-none-any.whl --cache-dir baz --reinstall --refresh ``` --- crates/uv-cache/src/lib.rs | 6 ++ .../src/distribution_database.rs | 85 +++++++++++++------ crates/uv/tests/pip_sync.rs | 2 +- 3 files changed, 68 insertions(+), 25 deletions(-) diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index 6aaea35a1..05082ac68 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -795,6 +795,12 @@ impl ArchiveTimestamp { } } + /// Return the modification timestamp for a file. + pub fn from_file(path: impl AsRef) -> Result { + let metadata = fs_err::metadata(path.as_ref())?; + Ok(Self::Exact(Timestamp::from_metadata(&metadata))) + } + /// Return the modification timestamp for an archive. pub fn timestamp(&self) -> Timestamp { match self { diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 5c63f5428..477a408b1 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -15,8 +15,9 @@ use distribution_types::{ }; use platform_tags::Tags; use pypi_types::Metadata23; -use uv_cache::{ArchiveTarget, ArchiveTimestamp, CacheBucket, CacheEntry, WheelCache}; +use uv_cache::{ArchiveTimestamp, CacheBucket, CacheEntry, CachedByTimestamp, WheelCache}; use uv_client::{CacheControl, CachedClientError, Connectivity, RegistryClient}; +use uv_fs::write_atomic; use uv_types::{BuildContext, NoBinary, NoBuild}; use crate::locks::Locks; @@ -471,30 +472,31 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> wheel_entry: CacheEntry, dist: &BuiltDist, ) -> Result { - // If the file is already unzipped, and the unzipped directory is fresh, - // return it. - match wheel_entry.path().canonicalize() { - Ok(archive) => { - if ArchiveTimestamp::up_to_date_with(path, ArchiveTarget::Cache(&archive)) - .map_err(Error::CacheRead)? - { - return Ok(LocalWheel { - dist: Dist::Built(dist.clone()), - archive, - filename: filename.clone(), - }); - } - } - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => return Err(Error::CacheRead(err)), - } + // Determine the last-modified time of the wheel. + let modified = ArchiveTimestamp::from_file(path).map_err(Error::CacheRead)?; - // Otherwise, unzip the wheel. - Ok(LocalWheel { - dist: Dist::Built(dist.clone()), - archive: self.unzip_wheel(path, wheel_entry.path()).await?, - filename: filename.clone(), - }) + // Attempt to read the archive pointer from the cache. + let archive_entry = wheel_entry.with_file(format!("{}.rev", filename.stem())); + let archive = read_timestamped_archive(&archive_entry, modified)?; + + // If the file is already unzipped, and the cache is up-to-date, return it. + if let Some(archive) = archive { + Ok(LocalWheel { + dist: Dist::Built(dist.clone()), + archive, + filename: filename.clone(), + }) + } else { + // Otherwise, unzip the wheel. + let archive = self.unzip_wheel(path, wheel_entry.path()).await?; + write_timestamped_archive(&archive_entry, archive.clone(), modified).await?; + + Ok(LocalWheel { + dist: Dist::Built(dist.clone()), + archive, + filename: filename.clone(), + }) + } } /// Unzip a wheel into the cache, returning the path to the unzipped directory. @@ -542,3 +544,38 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> self.build_context.index_locations() } } + +/// Write a timestamped archive path to the cache. +async fn write_timestamped_archive( + cache_entry: &CacheEntry, + data: PathBuf, + modified: ArchiveTimestamp, +) -> Result<(), Error> { + write_atomic( + cache_entry.path(), + rmp_serde::to_vec(&CachedByTimestamp { + timestamp: modified.timestamp(), + data, + })?, + ) + .await + .map_err(Error::CacheWrite) +} + +/// Read an existing timestamped archive path, if it exists and is up-to-date. +fn read_timestamped_archive( + cache_entry: &CacheEntry, + modified: ArchiveTimestamp, +) -> Result, Error> { + match fs_err::read(cache_entry.path()) { + Ok(cached) => { + let cached = rmp_serde::from_slice::>(&cached)?; + if cached.timestamp == modified.timestamp() { + return Ok(Some(cached.data)); + } + } + Err(err) if err.kind() == io::ErrorKind::NotFound => {} + Err(err) => return Err(Error::CacheRead(err)), + } + Ok(None) +} diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 79088af57..255912a11 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -1708,7 +1708,7 @@ fn install_path_built_dist_cached() -> Result<()> { ----- stdout ----- ----- stderr ----- - Removed 1 file for tomli ([SIZE]) + Removed 2 files for tomli ([SIZE]) "### ); From bdeab5519358735b2a4f403c25eec80cbe91daa9 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 8 Apr 2024 15:34:08 -0500 Subject: [PATCH 055/110] Add extract support for zstd (#2861) We need this to extract toolchain downloads --- Cargo.lock | 30 ++++++++++++++++++++++++++++ crates/uv-extract/Cargo.toml | 2 +- crates/uv-extract/src/seek.rs | 17 +++++++++++++++- crates/uv-extract/src/stream.rs | 35 +++++++++++++++++++++++++++++++-- 4 files changed, 80 insertions(+), 4 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 0cd3b6ce6..c8bc8a192 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -211,6 +211,8 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", + "zstd", + "zstd-safe", ] [[package]] @@ -5382,3 +5384,31 @@ dependencies = [ "crossbeam-utils", "flate2", ] + +[[package]] +name = "zstd" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d789b1514203a1120ad2429eae43a7bd32b90976a7bb8a05f7ec02fa88cc23a" +dependencies = [ + "zstd-safe", +] + +[[package]] +name = "zstd-safe" +version = "7.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cd99b45c6bc03a018c8b8a86025678c87e55526064e38f9df301989dce7ec0a" +dependencies = [ + "zstd-sys", +] + +[[package]] +name = "zstd-sys" +version = "2.0.10+zstd.1.5.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c253a4914af5bafc8fa8c86ee400827e83cf6ec01195ec1f1ed8441bf00d65aa" +dependencies = [ + "cc", + "pkg-config", +] diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index ca0704c4c..f40dee266 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -13,7 +13,7 @@ license = { workspace = true } workspace = true [dependencies] -async-compression = { workspace = true, features = ["gzip"] } +async-compression = { workspace = true, features = ["gzip", "zstd"] } async_zip = { workspace = true, features = ["tokio"] } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } diff --git a/crates/uv-extract/src/seek.rs b/crates/uv-extract/src/seek.rs index 636bb6318..57499fc86 100644 --- a/crates/uv-extract/src/seek.rs +++ b/crates/uv-extract/src/seek.rs @@ -107,7 +107,22 @@ pub async fn archive( .is_some_and(|ext| ext.eq_ignore_ascii_case("tar")) }) { - crate::stream::untar(reader, target).await?; + crate::stream::untar_gz(reader, target).await?; + return Ok(()); + } + + // `.tar.zst` + if source + .as_ref() + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("zst")) + && source.as_ref().file_stem().is_some_and(|stem| { + Path::new(stem) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("tar")) + }) + { + crate::stream::untar_zst(reader, target).await?; return Ok(()); } diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index 7fb4707fc..cff2825dc 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -151,7 +151,7 @@ async fn untar_in>( /// Unzip a `.tar.gz` archive into the target directory, without requiring `Seek`. /// /// This is useful for unpacking files as they're being downloaded. -pub async fn untar( +pub async fn untar_gz( reader: R, target: impl AsRef, ) -> Result<(), Error> { @@ -163,6 +163,22 @@ pub async fn untar( Ok(untar_in(&mut archive, target.as_ref()).await?) } +/// Unzip a `.tar.zst` archive into the target directory, without requiring `Seek`. +/// +/// This is useful for unpacking files as they're being downloaded. +pub async fn untar_zst( + reader: R, + target: impl AsRef, +) -> Result<(), Error> { + let reader = tokio::io::BufReader::new(reader); + let decompressed_bytes = async_compression::tokio::bufread::ZstdDecoder::new(reader); + + let mut archive = tokio_tar::ArchiveBuilder::new(decompressed_bytes) + .set_preserve_mtime(false) + .build(); + Ok(untar_in(&mut archive, target.as_ref()).await?) +} + /// Unzip a `.zip` or `.tar.gz` archive into the target directory, without requiring `Seek`. pub async fn archive( reader: R, @@ -190,7 +206,22 @@ pub async fn archive( .is_some_and(|ext| ext.eq_ignore_ascii_case("tar")) }) { - untar(reader, target).await?; + untar_gz(reader, target).await?; + return Ok(()); + } + + // `.tar.zst` + if source + .as_ref() + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("zst")) + && source.as_ref().file_stem().is_some_and(|stem| { + Path::new(stem) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("tar")) + }) + { + untar_zst(reader, target).await?; return Ok(()); } From cc3c5700e1a433f45eca11c09985f9b5e7f25c04 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 17:04:27 -0400 Subject: [PATCH 056/110] Use scheme parsing to determine absolute vs. relative URLs (#2904) ## Summary We have a heuristic in `File` that attempts to detect whether a URL is absolute or relative. However, `contains("://")` is prone to false positive. In the linked issues, the URLs look like: ``` /packages/5a/d8/4d75d1e4287ad9d051aab793c68f902c9c55c4397636b5ee540ebd15aedf/pytz-2005k.tar.bz2?hash=597b596dc1c2c130cd0a57a043459c3bd6477c640c07ac34ca3ce8eed7e6f30c&remote=https://files.pythonhosted.org/packages/5a/d8/4d75d1e4287ad9d051aab793c68f902c9c55c4397636b5ee540ebd15aedf/pytz-2005k.tar.bz2#sha256=597b596dc1c2c130cd0a57a043459c3bd6477c640c07ac34ca3ce8eed7e6f30c ``` Which is relative, but includes `://`. Instead, we should determine whether the URL has a _scheme_ which matches the `Url` crate internally. Closes https://github.com/astral-sh/uv/issues/2899. --- crates/distribution-types/src/file.rs | 11 ++++--- crates/pypi-types/src/base_url.rs | 42 ++++++++------------------- 2 files changed, 19 insertions(+), 34 deletions(-) diff --git a/crates/distribution-types/src/file.rs b/crates/distribution-types/src/file.rs index dd96e63ff..202202b44 100644 --- a/crates/distribution-types/src/file.rs +++ b/crates/distribution-types/src/file.rs @@ -6,6 +6,7 @@ use thiserror::Error; use url::Url; use pep440_rs::{VersionSpecifiers, VersionSpecifiersParseError}; +use pep508_rs::split_scheme; use pypi_types::{DistInfoMetadata, Hashes, Yanked}; /// Error converting [`pypi_types::File`] to [`distribution_type::File`]. @@ -51,10 +52,12 @@ impl File { .map_err(|err| FileConversionError::RequiresPython(err.line().clone(), err))?, size: file.size, upload_time_utc_ms: file.upload_time.map(|dt| dt.timestamp_millis()), - url: if file.url.contains("://") { - FileLocation::AbsoluteUrl(file.url) - } else { - FileLocation::RelativeUrl(base.to_string(), file.url) + url: { + if split_scheme(&file.url).is_some() { + FileLocation::AbsoluteUrl(file.url) + } else { + FileLocation::RelativeUrl(base.to_string(), file.url) + } }, yanked: file.yanked, }) diff --git a/crates/pypi-types/src/base_url.rs b/crates/pypi-types/src/base_url.rs index d4d991cdb..428920743 100644 --- a/crates/pypi-types/src/base_url.rs +++ b/crates/pypi-types/src/base_url.rs @@ -1,37 +1,19 @@ use serde::{Deserialize, Serialize}; use url::Url; -/// Join a possibly relative URL to a base URL. -/// -/// When `maybe_relative` is not relative, then it is parsed and returned with -/// `base` being ignored. -/// -/// This is useful for parsing URLs that may be absolute or relative, with a -/// known base URL, and that doesn't require having already parsed a `BaseUrl`. -pub fn base_url_join_relative(base: &str, maybe_relative: &str) -> Result { - match Url::parse(maybe_relative) { - Ok(absolute) => Ok(absolute), - Err(err) => { - if err == url::ParseError::RelativeUrlWithoutBase { - let base_url = Url::parse(base).map_err(|err| JoinRelativeError::ParseError { - original: base.to_string(), - source: err, - })?; +/// Join a relative URL to a base URL. +pub fn base_url_join_relative(base: &str, relative: &str) -> Result { + let base_url = Url::parse(base).map_err(|err| JoinRelativeError::ParseError { + original: base.to_string(), + source: err, + })?; - base_url - .join(maybe_relative) - .map_err(|_| JoinRelativeError::ParseError { - original: format!("{base}/{maybe_relative}"), - source: err, - }) - } else { - Err(JoinRelativeError::ParseError { - original: maybe_relative.to_string(), - source: err, - }) - } - } - } + base_url + .join(relative) + .map_err(|err| JoinRelativeError::ParseError { + original: format!("{base}/{relative}"), + source: err, + }) } /// An error that occurs when `base_url_join_relative` fails. From 538c88130e9004202244348ffdf63a5e8404ff9c Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 8 Apr 2024 16:06:55 -0500 Subject: [PATCH 057/110] Group `pyo3` dependency updates (#2889) Seems needed for https://github.com/astral-sh/uv/pull/2879 --- .github/renovate.json5 | 6 ++++++ Cargo.lock | 22 ++++++++++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/.github/renovate.json5 b/.github/renovate.json5 index 10de5afe0..73b666158 100644 --- a/.github/renovate.json5 +++ b/.github/renovate.json5 @@ -35,6 +35,12 @@ matchDepTypes: ["devDependencies"], description: "Weekly update of Rust development dependencies", }, + { + groupName: "pyo3", + matchManagers: ["cargo"], + matchPackagePatterns: ["pyo3"], + description: "Weekly update of pyo3 dependencies", + }, ], vulnerabilityAlerts: { commitMessageSuffix: "", diff --git a/Cargo.lock b/Cargo.lock index c8bc8a192..7d6e8ce20 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,9 +1784,9 @@ checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" [[package]] name = "insta" -version = "1.38.0" +version = "1.36.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc" +checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e" dependencies = [ "console", "lazy_static", @@ -1794,6 +1794,7 @@ dependencies = [ "regex", "serde", "similar", + "yaml-rust", ] [[package]] @@ -2804,9 +2805,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.10.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" dependencies = [ "either", "rayon-core", @@ -3864,9 +3865,9 @@ checksum = "b130bd8a58c163224b44e217b4239ca7b927d82bf6cc2fea1fc561d15056e3f7" [[package]] name = "tokio" -version = "1.37.0" +version = "1.36.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" +checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" dependencies = [ "backtrace", "bytes", @@ -5373,6 +5374,15 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "yaml-rust" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" +dependencies = [ + "linked-hash-map", +] + [[package]] name = "zip" version = "0.6.6" From f42013214af02f709e18e4b9928d9fc41cf52915 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 8 Apr 2024 16:25:33 -0500 Subject: [PATCH 058/110] Restore lockfile (#2914) Accidentally reverted the lockfile in https://github.com/astral-sh/uv/commit/538c88130e9004202244348ffdf63a5e8404ff9c Closes #2912 Closes #2910 Closes #2913 --- Cargo.lock | 22 ++++++---------------- 1 file changed, 6 insertions(+), 16 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7d6e8ce20..c8bc8a192 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,9 +1784,9 @@ checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" [[package]] name = "insta" -version = "1.36.1" +version = "1.38.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a7c22c4d34ef4788c351e971c52bfdfe7ea2766f8c5466bc175dd46e52ac22e" +checksum = "3eab73f58e59ca6526037208f0e98851159ec1633cf17b6cd2e1f2c3fd5d53cc" dependencies = [ "console", "lazy_static", @@ -1794,7 +1794,6 @@ dependencies = [ "regex", "serde", "similar", - "yaml-rust", ] [[package]] @@ -2805,9 +2804,9 @@ dependencies = [ [[package]] name = "rayon" -version = "1.9.0" +version = "1.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e4963ed1bc86e4f3ee217022bd855b297cef07fb9eac5dfa1f788b220b49b3bd" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" dependencies = [ "either", "rayon-core", @@ -3865,9 +3864,9 @@ checksum = "b130bd8a58c163224b44e217b4239ca7b927d82bf6cc2fea1fc561d15056e3f7" [[package]] name = "tokio" -version = "1.36.0" +version = "1.37.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61285f6515fa018fb2d1e46eb21223fff441ee8db5d0f1435e8ab4f5cdb80931" +checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787" dependencies = [ "backtrace", "bytes", @@ -5374,15 +5373,6 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" -[[package]] -name = "yaml-rust" -version = "0.4.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" -dependencies = [ - "linked-hash-map", -] - [[package]] name = "zip" version = "0.6.6" From 31860565f600a0a37198e2d750ec5153a84680aa Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Mon, 8 Apr 2024 16:33:31 -0500 Subject: [PATCH 059/110] Disable CentOS system check (#2916) This is broken (see https://github.com/astral-sh/uv/issues/2915) and not a priority since we have Amazon Linux coverage --- .github/workflows/ci.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f36c651cd..b5aabf16a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -346,6 +346,8 @@ jobs: run: python scripts/check_system_python.py --uv ./uv system-test-centos: + # https://github.com/astral-sh/uv/issues/2915 + if: false needs: build-binary-linux name: "check system | python on centos" runs-on: ubuntu-latest From 1ab471d1678d396bef79c4bb6107d48bedd5d91d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 19:49:23 -0400 Subject: [PATCH 060/110] Reduce visibility of some methods in source database (#2919) --- crates/uv-distribution/src/source/mod.rs | 156 +++++++++++------------ 1 file changed, 77 insertions(+), 79 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index f968b94ea..36da9e573 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1098,81 +1098,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } -#[derive(Debug)] -enum ExtractedSource { - /// The source distribution was passed in as a directory, and so doesn't need to be extracted. - Directory(PathBuf), - /// The source distribution was passed in as an archive, and was extracted into a temporary - /// directory. - /// - /// The extracted archive and temporary directory will be deleted when the `ExtractedSource` is - /// dropped. - #[allow(dead_code)] - Archive(PathBuf, TempDir), -} - -impl ExtractedSource { - /// Return the [`Path`] to the extracted source root. - fn path(&self) -> &Path { - match self { - ExtractedSource::Directory(path) => path, - ExtractedSource::Archive(path, _) => path, - } - } -} - -/// Read the [`Metadata23`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2 -/// or later _and_ none of the required fields (`Requires-Python`, `Requires-Dist`, and -/// `Provides-Extra`) are marked as dynamic. -pub(crate) async fn read_pkg_info( - source_tree: &Path, - subdirectory: Option<&Path>, -) -> Result { - // Read the `PKG-INFO` file. - let pkg_info = match subdirectory { - Some(subdirectory) => source_tree.join(subdirectory).join("PKG-INFO"), - None => source_tree.join("PKG-INFO"), - }; - let content = match fs::read(pkg_info).await { - Ok(content) => content, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(Error::MissingPkgInfo); - } - Err(err) => return Err(Error::CacheRead(err)), - }; - - // Parse the metadata. - let metadata = Metadata23::parse_pkg_info(&content).map_err(Error::DynamicPkgInfo)?; - - Ok(metadata) -} - -/// Read the [`Metadata23`] from a source distribution's `pyproject.tom` file, if it defines static -/// metadata consistent with PEP 621. -pub(crate) async fn read_pyproject_toml( - source_tree: &Path, - subdirectory: Option<&Path>, -) -> Result { - // Read the `pyproject.toml` file. - let pyproject_toml = match subdirectory { - Some(subdirectory) => source_tree.join(subdirectory).join("pyproject.toml"), - None => source_tree.join("pyproject.toml"), - }; - let content = match fs::read_to_string(pyproject_toml).await { - Ok(content) => content, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(Error::MissingPyprojectToml); - } - Err(err) => return Err(Error::CacheRead(err)), - }; - - // Parse the metadata. - let metadata = - Metadata23::parse_pyproject_toml(&content).map_err(Error::DynamicPyprojectToml)?; - - Ok(metadata) -} - /// Read an existing HTTP-cached [`Manifest`], if it exists. pub(crate) fn read_http_manifest(cache_entry: &CacheEntry) -> Result, Error> { match fs_err::File::open(cache_entry.path()) { @@ -1206,10 +1131,85 @@ pub(crate) fn read_timestamp_manifest( Ok(None) } +#[derive(Debug)] +enum ExtractedSource { + /// The source distribution was passed in as a directory, and so doesn't need to be extracted. + Directory(PathBuf), + /// The source distribution was passed in as an archive, and was extracted into a temporary + /// directory. + /// + /// The extracted archive and temporary directory will be deleted when the `ExtractedSource` is + /// dropped. + #[allow(dead_code)] + Archive(PathBuf, TempDir), +} + +impl ExtractedSource { + /// Return the [`Path`] to the extracted source root. + fn path(&self) -> &Path { + match self { + ExtractedSource::Directory(path) => path, + ExtractedSource::Archive(path, _) => path, + } + } +} + +/// Read the [`Metadata23`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2 +/// or later _and_ none of the required fields (`Requires-Python`, `Requires-Dist`, and +/// `Provides-Extra`) are marked as dynamic. +async fn read_pkg_info( + source_tree: &Path, + subdirectory: Option<&Path>, +) -> Result { + // Read the `PKG-INFO` file. + let pkg_info = match subdirectory { + Some(subdirectory) => source_tree.join(subdirectory).join("PKG-INFO"), + None => source_tree.join("PKG-INFO"), + }; + let content = match fs::read(pkg_info).await { + Ok(content) => content, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Err(Error::MissingPkgInfo); + } + Err(err) => return Err(Error::CacheRead(err)), + }; + + // Parse the metadata. + let metadata = Metadata23::parse_pkg_info(&content).map_err(Error::DynamicPkgInfo)?; + + Ok(metadata) +} + +/// Read the [`Metadata23`] from a source distribution's `pyproject.tom` file, if it defines static +/// metadata consistent with PEP 621. +async fn read_pyproject_toml( + source_tree: &Path, + subdirectory: Option<&Path>, +) -> Result { + // Read the `pyproject.toml` file. + let pyproject_toml = match subdirectory { + Some(subdirectory) => source_tree.join(subdirectory).join("pyproject.toml"), + None => source_tree.join("pyproject.toml"), + }; + let content = match fs::read_to_string(pyproject_toml).await { + Ok(content) => content, + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Err(Error::MissingPyprojectToml); + } + Err(err) => return Err(Error::CacheRead(err)), + }; + + // Parse the metadata. + let metadata = + Metadata23::parse_pyproject_toml(&content).map_err(Error::DynamicPyprojectToml)?; + + Ok(metadata) +} + /// Read an existing timestamped [`Manifest`], if it exists and is up-to-date. /// /// If the cache entry is stale, a new entry will be created. -pub(crate) async fn refresh_timestamp_manifest( +async fn refresh_timestamp_manifest( cache_entry: &CacheEntry, freshness: Freshness, modified: ArchiveTimestamp, @@ -1239,9 +1239,7 @@ pub(crate) async fn refresh_timestamp_manifest( } /// Read an existing cached [`Metadata23`], if it exists. -pub(crate) async fn read_cached_metadata( - cache_entry: &CacheEntry, -) -> Result, Error> { +async fn read_cached_metadata(cache_entry: &CacheEntry) -> Result, Error> { match fs::read(&cache_entry.path()).await { Ok(cached) => Ok(Some(rmp_serde::from_slice::(&cached)?)), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), From 4f14e2a764ab4964b868f965387277205376bf86 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 20:00:57 -0400 Subject: [PATCH 061/110] Rebrand `Manifest` as `Revision` in wheel database (#2920) ## Summary I think this is a much clearer name for this concept: the set of "versions" of a given wheel or source distribution. We also use "Manifest" elsewhere to refer to the set of requirements, constraints, etc., so this was overloaded. --- .../src/index/built_wheel_index.rs | 18 +-- .../src/index/registry_wheel_index.rs | 8 +- .../src/source/built_wheel_metadata.rs | 2 +- crates/uv-distribution/src/source/manifest.rs | 17 --- crates/uv-distribution/src/source/mod.rs | 108 +++++++++--------- crates/uv-distribution/src/source/revision.rs | 22 ++++ 6 files changed, 91 insertions(+), 84 deletions(-) delete mode 100644 crates/uv-distribution/src/source/manifest.rs create mode 100644 crates/uv-distribution/src/source/revision.rs diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index 6dde63ebe..df60c51ee 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -4,7 +4,7 @@ use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, CacheShard, WheelCache}; use uv_fs::symlinks; use crate::index::cached_wheel::CachedWheel; -use crate::source::{read_http_manifest, read_timestamp_manifest, MANIFEST}; +use crate::source::{read_http_revision, read_timestamped_revision, REVISION}; use crate::Error; /// A local index of built distributions for a specific source distribution. @@ -26,14 +26,14 @@ impl BuiltWheelIndex { WheelCache::Url(source_dist.url.raw()).root(), ); - // Read the manifest from the cache. There's no need to enforce freshness, since we + // Read the revision from the cache. There's no need to enforce freshness, since we // enforce freshness on the entries. - let manifest_entry = cache_shard.entry(MANIFEST); - let Some(manifest) = read_http_manifest(&manifest_entry)? else { + let revision_entry = cache_shard.entry(REVISION); + let Some(revision) = read_http_revision(&revision_entry)? else { return Ok(None); }; - Ok(Self::find(&cache_shard.shard(manifest.id()), tags)) + Ok(Self::find(&cache_shard.shard(revision.id()), tags)) } /// Return the most compatible [`CachedWheel`] for a given source distribution at a local path. @@ -54,14 +54,14 @@ impl BuiltWheelIndex { return Err(Error::DirWithoutEntrypoint); }; - // Read the manifest from the cache. There's no need to enforce freshness, since we + // Read the revision from the cache. There's no need to enforce freshness, since we // enforce freshness on the entries. - let manifest_entry = cache_shard.entry(MANIFEST); - let Some(manifest) = read_timestamp_manifest(&manifest_entry, modified)? else { + let revision_entry = cache_shard.entry(REVISION); + let Some(revision) = read_timestamped_revision(&revision_entry, modified)? else { return Ok(None); }; - Ok(Self::find(&cache_shard.shard(manifest.id()), tags)) + Ok(Self::find(&cache_shard.shard(revision.id()), tags)) } /// Return the most compatible [`CachedWheel`] for a given source distribution at a git URL. diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 8342cdb8a..fc5fceb18 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -13,7 +13,7 @@ use uv_fs::{directories, symlinks}; use uv_normalize::PackageName; use crate::index::cached_wheel::CachedWheel; -use crate::source::{read_http_manifest, MANIFEST}; +use crate::source::{read_http_revision, REVISION}; /// A local index of distributions that originate from a registry, like `PyPI`. #[derive(Debug)] @@ -113,9 +113,9 @@ impl<'a> RegistryWheelIndex<'a> { for shard in directories(&cache_shard) { // Read the existing metadata from the cache, if it exists. let cache_shard = cache_shard.shard(shard); - let manifest_entry = cache_shard.entry(MANIFEST); - if let Ok(Some(manifest)) = read_http_manifest(&manifest_entry) { - Self::add_directory(cache_shard.join(manifest.id()), tags, &mut versions); + let revision_entry = cache_shard.entry(REVISION); + if let Ok(Some(revision)) = read_http_revision(&revision_entry) { + Self::add_directory(cache_shard.join(revision.id()), tags, &mut versions); }; } } diff --git a/crates/uv-distribution/src/source/built_wheel_metadata.rs b/crates/uv-distribution/src/source/built_wheel_metadata.rs index a968b88d1..3115d9882 100644 --- a/crates/uv-distribution/src/source/built_wheel_metadata.rs +++ b/crates/uv-distribution/src/source/built_wheel_metadata.rs @@ -18,7 +18,7 @@ pub struct BuiltWheelMetadata { } impl BuiltWheelMetadata { - /// Find a compatible wheel in the cache based on the given manifest. + /// Find a compatible wheel in the cache. pub(crate) fn find_in_cache(tags: &Tags, cache_shard: &CacheShard) -> Option { for directory in files(cache_shard) { if let Some(metadata) = Self::from_path(directory, cache_shard) { diff --git a/crates/uv-distribution/src/source/manifest.rs b/crates/uv-distribution/src/source/manifest.rs deleted file mode 100644 index ba1085986..000000000 --- a/crates/uv-distribution/src/source/manifest.rs +++ /dev/null @@ -1,17 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// The [`Manifest`] is a thin wrapper around a unique identifier for the source distribution. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct Manifest(String); - -impl Manifest { - /// Initialize a new [`Manifest`] with a random UUID. - pub(crate) fn new() -> Self { - Self(nanoid::nanoid!()) - } - - /// Return the unique ID of the manifest. - pub(crate) fn id(&self) -> &str { - &self.0 - } -} diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 36da9e573..1942ab82b 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -36,11 +36,11 @@ use uv_types::{BuildContext, BuildKind, NoBuild, SourceBuildTrait}; use crate::error::Error; use crate::git::{fetch_git_archive, resolve_precise}; use crate::source::built_wheel_metadata::BuiltWheelMetadata; -use crate::source::manifest::Manifest; +use crate::source::revision::Revision; use crate::Reporter; mod built_wheel_metadata; -mod manifest; +mod revision; /// Fetch and build a source distribution from a remote source, or from a local cache. pub struct SourceDistributionBuilder<'a, T: BuildContext> { @@ -49,8 +49,10 @@ pub struct SourceDistributionBuilder<'a, T: BuildContext> { reporter: Option>, } -/// The name of the file that contains the cached manifest, encoded via `MsgPack`. -pub(crate) const MANIFEST: &str = "manifest.msgpack"; +/// The name of the file that contains the revision ID, encoded via `MsgPack`. +/// +/// TODO(charlie): Update the filename whenever we bump the cache version. +pub(crate) const REVISION: &str = "manifest.msgpack"; /// The name of the file that contains the cached distribution metadata, encoded via `MsgPack`. pub(crate) const METADATA: &str = "metadata.msgpack"; @@ -328,7 +330,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { subdirectory: Option<&'data Path>, tags: &Tags, ) -> Result { - let cache_entry = cache_shard.entry(MANIFEST); + let cache_entry = cache_shard.entry(REVISION); let cache_control = match self.client.connectivity() { Connectivity::Online => CacheControl::from( self.build_context @@ -342,22 +344,22 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let download = |response| { async { // At this point, we're seeing a new or updated source distribution. Initialize a - // new manifest, to collect the source and built artifacts. - let manifest = Manifest::new(); + // new revision, to collect the source and built artifacts. + let revision = Revision::new(); // Download the source distribution. debug!("Downloading source distribution: {source}"); - let source_dist_entry = cache_shard.shard(manifest.id()).entry(filename); + let source_dist_entry = cache_shard.shard(revision.id()).entry(filename); self.persist_url(response, source, filename, &source_dist_entry) .await?; - Ok(manifest) + Ok(revision) } .boxed() .instrument(info_span!("download", source_dist = %source)) }; let req = self.request(url.clone())?; - let manifest = self + let revision = self .client .cached_client() .get_serde(req, &cache_entry, cache_control, download) @@ -367,11 +369,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { CachedClientError::Client(err) => Error::Client(err), })?; - // From here on, scope all operations to the current build. Within the manifest shard, + // From here on, scope all operations to the current build. Within the revision shard, // there's no need to check for freshness, since entries have to be fresher than the - // manifest itself. There's also no need to lock, since we never replace entries within the + // revision itself. There's also no need to lock, since we never replace entries within the // shard. - let cache_shard = cache_shard.shard(manifest.id()); + let cache_shard = cache_shard.shard(revision.id()); // If the cache contains a compatible wheel, return it. if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) { @@ -421,7 +423,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { cache_shard: &CacheShard, subdirectory: Option<&'data Path>, ) -> Result { - let cache_entry = cache_shard.entry(MANIFEST); + let cache_entry = cache_shard.entry(REVISION); let cache_control = match self.client.connectivity() { Connectivity::Online => CacheControl::from( self.build_context @@ -435,22 +437,22 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let download = |response| { async { // At this point, we're seeing a new or updated source distribution. Initialize a - // new manifest, to collect the source and built artifacts. - let manifest = Manifest::new(); + // new revision, to collect the source and built artifacts. + let revision = Revision::new(); // Download the source distribution. debug!("Downloading source distribution: {source}"); - let source_dist_entry = cache_shard.shard(manifest.id()).entry(filename); + let source_dist_entry = cache_shard.shard(revision.id()).entry(filename); self.persist_url(response, source, filename, &source_dist_entry) .await?; - Ok(manifest) + Ok(revision) } .boxed() .instrument(info_span!("download", source_dist = %source)) }; let req = self.request(url.clone())?; - let manifest = self + let revision = self .client .cached_client() .get_serde(req, &cache_entry, cache_control, download) @@ -460,11 +462,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { CachedClientError::Client(err) => Error::Client(err), })?; - // From here on, scope all operations to the current build. Within the manifest shard, + // From here on, scope all operations to the current build. Within the revision shard, // there's no need to check for freshness, since entries have to be fresher than the - // manifest itself. There's also no need to lock, since we never replace entries within the + // revision itself. There's also no need to lock, since we never replace entries within the // shard. - let cache_shard = cache_shard.shard(manifest.id()); + let cache_shard = cache_shard.shard(revision.id()); // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); @@ -540,20 +542,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // Read the existing metadata from the cache. - let manifest_entry = cache_shard.entry(MANIFEST); - let manifest_freshness = self + let revision_entry = cache_shard.entry(REVISION); + let revision_freshness = self .build_context .cache() - .freshness(&manifest_entry, source.name()) + .freshness(&revision_entry, source.name()) .map_err(Error::CacheRead)?; - let manifest = - refresh_timestamp_manifest(&manifest_entry, manifest_freshness, modified).await?; + let revision = + refresh_timestamped_revision(&revision_entry, revision_freshness, modified).await?; - // From here on, scope all operations to the current build. Within the manifest shard, + // From here on, scope all operations to the current build. Within the revision shard, // there's no need to check for freshness, since entries have to be fresher than the - // manifest itself. There's also no need to lock, since we never replace entries within the + // revision itself. There's also no need to lock, since we never replace entries within the // shard. - let cache_shard = cache_shard.shard(manifest.id()); + let cache_shard = cache_shard.shard(revision.id()); // If the cache contains a compatible wheel, return it. if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) { @@ -612,20 +614,20 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // Read the existing metadata from the cache, to clear stale entries. - let manifest_entry = cache_shard.entry(MANIFEST); - let manifest_freshness = self + let revision_entry = cache_shard.entry(REVISION); + let revision_freshness = self .build_context .cache() - .freshness(&manifest_entry, source.name()) + .freshness(&revision_entry, source.name()) .map_err(Error::CacheRead)?; - let manifest = - refresh_timestamp_manifest(&manifest_entry, manifest_freshness, modified).await?; + let revision = + refresh_timestamped_revision(&revision_entry, revision_freshness, modified).await?; - // From here on, scope all operations to the current build. Within the manifest shard, + // From here on, scope all operations to the current build. Within the revision shard, // there's no need to check for freshness, since entries have to be fresher than the - // manifest itself. There's also no need to lock, since we never replace entries within the + // revision itself. There's also no need to lock, since we never replace entries within the // shard. - let cache_shard = cache_shard.shard(manifest.id()); + let cache_shard = cache_shard.shard(revision.id()); // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); @@ -1098,29 +1100,29 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } -/// Read an existing HTTP-cached [`Manifest`], if it exists. -pub(crate) fn read_http_manifest(cache_entry: &CacheEntry) -> Result, Error> { +/// Read an existing HTTP-cached [`Revision`], if it exists. +pub(crate) fn read_http_revision(cache_entry: &CacheEntry) -> Result, Error> { match fs_err::File::open(cache_entry.path()) { Ok(file) => { let data = DataWithCachePolicy::from_reader(file)?.data; - Ok(Some(rmp_serde::from_slice::(&data)?)) + Ok(Some(rmp_serde::from_slice::(&data)?)) } Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), Err(err) => Err(Error::CacheRead(err)), } } -/// Read an existing timestamped [`Manifest`], if it exists and is up-to-date. +/// Read an existing timestamped [`Revision`], if it exists and is up-to-date. /// /// If the cache entry is stale, a new entry will be created. -pub(crate) fn read_timestamp_manifest( +pub(crate) fn read_timestamped_revision( cache_entry: &CacheEntry, modified: ArchiveTimestamp, -) -> Result, Error> { +) -> Result, Error> { // If the cache entry is up-to-date, return it. match fs_err::read(cache_entry.path()) { Ok(cached) => { - let cached = rmp_serde::from_slice::>(&cached)?; + let cached = rmp_serde::from_slice::>(&cached)?; if cached.timestamp == modified.timestamp() { return Ok(Some(cached.data)); } @@ -1209,20 +1211,20 @@ async fn read_pyproject_toml( /// Read an existing timestamped [`Manifest`], if it exists and is up-to-date. /// /// If the cache entry is stale, a new entry will be created. -async fn refresh_timestamp_manifest( +async fn refresh_timestamped_revision( cache_entry: &CacheEntry, freshness: Freshness, modified: ArchiveTimestamp, -) -> Result { +) -> Result { // If we know the exact modification time, we don't need to force a revalidate. if matches!(modified, ArchiveTimestamp::Exact(_)) || freshness.is_fresh() { - if let Some(manifest) = read_timestamp_manifest(cache_entry, modified)? { - return Ok(manifest); + if let Some(revision) = read_timestamped_revision(cache_entry, modified)? { + return Ok(revision); } } - // Otherwise, create a new manifest. - let manifest = Manifest::new(); + // Otherwise, create a new revision. + let revision = Revision::new(); fs::create_dir_all(&cache_entry.dir()) .await .map_err(Error::CacheWrite)?; @@ -1230,12 +1232,12 @@ async fn refresh_timestamp_manifest( cache_entry.path(), rmp_serde::to_vec(&CachedByTimestamp { timestamp: modified.timestamp(), - data: manifest.clone(), + data: revision.clone(), })?, ) .await .map_err(Error::CacheWrite)?; - Ok(manifest) + Ok(revision) } /// Read an existing cached [`Metadata23`], if it exists. diff --git a/crates/uv-distribution/src/source/revision.rs b/crates/uv-distribution/src/source/revision.rs new file mode 100644 index 000000000..b2f6d5b9a --- /dev/null +++ b/crates/uv-distribution/src/source/revision.rs @@ -0,0 +1,22 @@ +use serde::{Deserialize, Serialize}; + +/// The [`Revision`] is a thin wrapper around a unique identifier for the source distribution. +/// +/// A revision represents a unique version of a source distribution, at a level more granular than +/// (e.g.) the version number of the distribution itself. For example, a source distribution hosted +/// at a URL or a local file path may have multiple revisions, each representing a unique state of +/// the distribution, despite the reported version number remaining the same. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct Revision(String); + +impl Revision { + /// Initialize a new [`Revision`] with a random UUID. + pub(crate) fn new() -> Self { + Self(nanoid::nanoid!()) + } + + /// Return the unique ID of the revision. + pub(crate) fn id(&self) -> &str { + &self.0 + } +} From 06e96a8f582583e892323e87749c81649f693b1f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 20:14:42 -0400 Subject: [PATCH 062/110] DRY up source distribution fetching between wheel and metadata routes (#2921) These will get more involved with hash-checking, so easiest to extract them now. No functional changes. --- crates/uv-distribution/src/source/mod.rs | 188 ++++++++++------------- 1 file changed, 82 insertions(+), 106 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 1942ab82b..e9c713360 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -330,44 +330,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { subdirectory: Option<&'data Path>, tags: &Tags, ) -> Result { - let cache_entry = cache_shard.entry(REVISION); - let cache_control = match self.client.connectivity() { - Connectivity::Online => CacheControl::from( - self.build_context - .cache() - .freshness(&cache_entry, source.name()) - .map_err(Error::CacheRead)?, - ), - Connectivity::Offline => CacheControl::AllowStale, - }; - - let download = |response| { - async { - // At this point, we're seeing a new or updated source distribution. Initialize a - // new revision, to collect the source and built artifacts. - let revision = Revision::new(); - - // Download the source distribution. - debug!("Downloading source distribution: {source}"); - let source_dist_entry = cache_shard.shard(revision.id()).entry(filename); - self.persist_url(response, source, filename, &source_dist_entry) - .await?; - - Ok(revision) - } - .boxed() - .instrument(info_span!("download", source_dist = %source)) - }; - let req = self.request(url.clone())?; + // Fetch the revision for the source distribution. let revision = self - .client - .cached_client() - .get_serde(req, &cache_entry, cache_control, download) - .await - .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), - })?; + .url_revision(source, filename, url, cache_shard) + .await?; // From here on, scope all operations to the current build. Within the revision shard, // there's no need to check for freshness, since entries have to be fresher than the @@ -423,44 +389,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { cache_shard: &CacheShard, subdirectory: Option<&'data Path>, ) -> Result { - let cache_entry = cache_shard.entry(REVISION); - let cache_control = match self.client.connectivity() { - Connectivity::Online => CacheControl::from( - self.build_context - .cache() - .freshness(&cache_entry, source.name()) - .map_err(Error::CacheRead)?, - ), - Connectivity::Offline => CacheControl::AllowStale, - }; - - let download = |response| { - async { - // At this point, we're seeing a new or updated source distribution. Initialize a - // new revision, to collect the source and built artifacts. - let revision = Revision::new(); - - // Download the source distribution. - debug!("Downloading source distribution: {source}"); - let source_dist_entry = cache_shard.shard(revision.id()).entry(filename); - self.persist_url(response, source, filename, &source_dist_entry) - .await?; - - Ok(revision) - } - .boxed() - .instrument(info_span!("download", source_dist = %source)) - }; - let req = self.request(url.clone())?; + // Fetch the revision for the source distribution. let revision = self - .client - .cached_client() - .get_serde(req, &cache_entry, cache_control, download) - .await - .map_err(|err| match err { - CachedClientError::Callback(err) => err, - CachedClientError::Client(err) => Error::Client(err), - })?; + .url_revision(source, filename, url, cache_shard) + .await?; // From here on, scope all operations to the current build. Within the revision shard, // there's no need to check for freshness, since entries have to be fresher than the @@ -521,6 +453,53 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(metadata) } + /// Return the [`Revision`] for a remote URL, refreshing it if necessary. + async fn url_revision( + &self, + source: &BuildableSource<'_>, + filename: &str, + url: &Url, + cache_shard: &CacheShard, + ) -> Result { + let cache_entry = cache_shard.entry(REVISION); + let cache_control = match self.client.connectivity() { + Connectivity::Online => CacheControl::from( + self.build_context + .cache() + .freshness(&cache_entry, source.name()) + .map_err(Error::CacheRead)?, + ), + Connectivity::Offline => CacheControl::AllowStale, + }; + + let download = |response| { + async { + // At this point, we're seeing a new or updated source distribution. Initialize a + // new revision, to collect the source and built artifacts. + let revision = Revision::new(); + + // Download the source distribution. + debug!("Downloading source distribution: {source}"); + let source_dist_entry = cache_shard.shard(revision.id()).entry(filename); + self.persist_url(response, source, filename, &source_dist_entry) + .await?; + + Ok(revision) + } + .boxed() + .instrument(info_span!("download", source_dist = %source)) + }; + let req = self.request(url.clone())?; + self.client + .cached_client() + .get_serde(req, &cache_entry, cache_control, download) + .await + .map_err(|err| match err { + CachedClientError::Callback(err) => err, + CachedClientError::Client(err) => Error::Client(err), + }) + } + /// Build a source distribution from a local path. async fn path( &self, @@ -534,22 +513,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { WheelCache::Path(resource.url).root(), ); - // Determine the last-modified time of the source distribution. - let Some(modified) = - ArchiveTimestamp::from_path(&resource.path).map_err(Error::CacheRead)? - else { - return Err(Error::DirWithoutEntrypoint); - }; - - // Read the existing metadata from the cache. - let revision_entry = cache_shard.entry(REVISION); - let revision_freshness = self - .build_context - .cache() - .freshness(&revision_entry, source.name()) - .map_err(Error::CacheRead)?; - let revision = - refresh_timestamped_revision(&revision_entry, revision_freshness, modified).await?; + // Fetch the revision for the source distribution. + let revision = self.path_revision(source, resource, &cache_shard).await?; // From here on, scope all operations to the current build. Within the revision shard, // there's no need to check for freshness, since entries have to be fresher than the @@ -606,22 +571,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { WheelCache::Path(resource.url).root(), ); - // Determine the last-modified time of the source distribution. - let Some(modified) = - ArchiveTimestamp::from_path(&resource.path).map_err(Error::CacheRead)? - else { - return Err(Error::DirWithoutEntrypoint); - }; - - // Read the existing metadata from the cache, to clear stale entries. - let revision_entry = cache_shard.entry(REVISION); - let revision_freshness = self - .build_context - .cache() - .freshness(&revision_entry, source.name()) - .map_err(Error::CacheRead)?; - let revision = - refresh_timestamped_revision(&revision_entry, revision_freshness, modified).await?; + // Fetch the revision for the source distribution. + let revision = self.path_revision(source, resource, &cache_shard).await?; // From here on, scope all operations to the current build. Within the revision shard, // there's no need to check for freshness, since entries have to be fresher than the @@ -686,6 +637,31 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(metadata) } + /// Return the [`Revision`] for a local path, refreshing it if necessary. + async fn path_revision( + &self, + source: &BuildableSource<'_>, + resource: &PathSourceUrl<'_>, + cache_shard: &CacheShard, + ) -> Result { + // Determine the last-modified time of the source distribution. + let Some(modified) = + ArchiveTimestamp::from_path(&resource.path).map_err(Error::CacheRead)? + else { + return Err(Error::DirWithoutEntrypoint); + }; + + // Read the existing metadata from the cache. + let revision_entry = cache_shard.entry(REVISION); + let revision_freshness = self + .build_context + .cache() + .freshness(&revision_entry, source.name()) + .map_err(Error::CacheRead)?; + + refresh_timestamped_revision(&revision_entry, revision_freshness, modified).await + } + /// Build a source distribution from a Git repository. async fn git( &self, From 07e3694c3cfdd399152ed1bd6f97341d1ffd0dc6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Mon, 8 Apr 2024 21:12:33 -0400 Subject: [PATCH 063/110] Separate local archive vs. local source tree paths in source database (#2922) ## Summary When you specify a source distribution via a path, it can either be a path to an archive (like a `.tar.gz` file), or a source tree (a directory). Right now, we handle both paths through the same methods in the source database. This PR splits them up into separate handlers. This will make hash generation a little easier, since we need to generate hashes for archives, but _can't_ generate hashes for source trees. It also means that we can now store the unzipped source distribution in the cache (in the case of archives), and avoid unzipping the source distribution needlessly on every invocation; and, overall, let's un enforce clearer expectations between the two routes (e.g., what errors are possible vs. not), at the cost of duplicating some code. Closes #2760 (incidentally -- not exactly the motivation for the change, but it did accomplish it). --- crates/uv-distribution/src/source/mod.rs | 430 +++++++++++++++-------- crates/uv/tests/pip_sync.rs | 2 +- 2 files changed, 286 insertions(+), 146 deletions(-) diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index e9c713360..59c86adb3 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -9,7 +9,6 @@ use anyhow::Result; use fs_err::tokio as fs; use futures::{FutureExt, TryStreamExt}; use reqwest::Response; -use tempfile::TempDir; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{debug, info_span, instrument, Instrument}; use url::Url; @@ -24,8 +23,7 @@ use install_wheel_rs::metadata::read_archive_metadata; use platform_tags::Tags; use pypi_types::Metadata23; use uv_cache::{ - ArchiveTimestamp, Cache, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Freshness, - WheelCache, + ArchiveTimestamp, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Freshness, WheelCache, }; use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, @@ -93,18 +91,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } FileLocation::Path(path) => { let url = Url::from_file_path(path).expect("path is absolute"); - - // If necessary, extract the archive. - let extracted = extract_archive(path, self.build_context.cache()).await?; - return self - .path( + .archive( source, &PathSourceUrl { url: &url, path: Cow::Borrowed(path), }, - extracted.path(), tags, ) .boxed() @@ -152,12 +145,15 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await? } BuildableSource::Dist(SourceDist::Path(dist)) => { - // If necessary, extract the archive. - let extracted = extract_archive(&dist.path, self.build_context.cache()).await?; - - self.path(source, &PathSourceUrl::from(dist), extracted.path(), tags) - .boxed() - .await? + if dist.path.is_dir() { + self.source_tree(source, &PathSourceUrl::from(dist), tags) + .boxed() + .await? + } else { + self.archive(source, &PathSourceUrl::from(dist), tags) + .boxed() + .await? + } } BuildableSource::Url(SourceUrl::Direct(resource)) => { let filename = resource @@ -187,12 +183,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.git(source, resource, tags).boxed().await? } BuildableSource::Url(SourceUrl::Path(resource)) => { - // If necessary, extract the archive. - let extracted = extract_archive(&resource.path, self.build_context.cache()).await?; - - self.path(source, resource, extracted.path(), tags) - .boxed() - .await? + if resource.path.is_dir() { + self.source_tree(source, resource, tags).boxed().await? + } else { + self.archive(source, resource, tags).boxed().await? + } } }; @@ -217,18 +212,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } FileLocation::Path(path) => { let url = Url::from_file_path(path).expect("path is absolute"); - - // If necessary, extract the archive. - let extracted = extract_archive(path, self.build_context.cache()).await?; - return self - .path_metadata( + .archive_metadata( source, &PathSourceUrl { url: &url, path: Cow::Borrowed(path), }, - extracted.path(), ) .boxed() .await; @@ -273,12 +263,15 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await? } BuildableSource::Dist(SourceDist::Path(dist)) => { - // If necessary, extract the archive. - let extracted = extract_archive(&dist.path, self.build_context.cache()).await?; - - self.path_metadata(source, &PathSourceUrl::from(dist), extracted.path()) - .boxed() - .await? + if dist.path.is_dir() { + self.source_tree_metadata(source, &PathSourceUrl::from(dist)) + .boxed() + .await? + } else { + self.archive_metadata(source, &PathSourceUrl::from(dist)) + .boxed() + .await? + } } BuildableSource::Url(SourceUrl::Direct(resource)) => { let filename = resource @@ -307,12 +300,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.git_metadata(source, resource).boxed().await? } BuildableSource::Url(SourceUrl::Path(resource)) => { - // If necessary, extract the archive. - let extracted = extract_archive(&resource.path, self.build_context.cache()).await?; - - self.path_metadata(source, resource, extracted.path()) - .boxed() - .await? + if resource.path.is_dir() { + self.source_tree_metadata(source, resource).boxed().await? + } else { + self.archive_metadata(source, resource).boxed().await? + } } }; @@ -335,10 +327,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .url_revision(source, filename, url, cache_shard) .await?; - // From here on, scope all operations to the current build. Within the revision shard, - // there's no need to check for freshness, since entries have to be fresher than the - // revision itself. There's also no need to lock, since we never replace entries within the - // shard. + // Scope all operations to the revision. Within the revision, there's no need to check for + // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); // If the cache contains a compatible wheel, return it. @@ -394,10 +384,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .url_revision(source, filename, url, cache_shard) .await?; - // From here on, scope all operations to the current build. Within the revision shard, - // there's no need to check for freshness, since entries have to be fresher than the - // revision itself. There's also no need to lock, since we never replace entries within the - // shard. + // Scope all operations to the revision. Within the revision, there's no need to check for + // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); // If the cache contains compatible metadata, return it. @@ -500,12 +488,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }) } - /// Build a source distribution from a local path. - async fn path( + /// Build a source distribution from a local archive (e.g., `.tar.gz` or `.zip`). + async fn archive( &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, - source_root: &Path, tags: &Tags, ) -> Result { let cache_shard = self.build_context.cache().shard( @@ -514,12 +501,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { ); // Fetch the revision for the source distribution. - let revision = self.path_revision(source, resource, &cache_shard).await?; + let revision = self + .archive_revision(source, resource, &cache_shard) + .await?; - // From here on, scope all operations to the current build. Within the revision shard, - // there's no need to check for freshness, since entries have to be fresher than the - // revision itself. There's also no need to lock, since we never replace entries within the - // shard. + // Scope all operations to the revision. Within the revision, there's no need to check for + // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); // If the cache contains a compatible wheel, return it. @@ -527,6 +514,8 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { return Ok(built_wheel); } + let source_entry = cache_shard.entry("source"); + // Otherwise, we need to build a wheel. let task = self .reporter @@ -534,7 +523,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .map(|reporter| reporter.on_build_start(source)); let (disk_filename, filename, metadata) = self - .build_distribution(source, source_root, None, &cache_shard) + .build_distribution(source, source_entry.path(), None, &cache_shard) .await?; if let Some(task) = task { @@ -556,15 +545,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }) } - /// Build the source distribution's metadata from a local path. + /// Build the source distribution's metadata from a local archive (e.g., `.tar.gz` or `.zip`). /// /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid /// building the wheel. - async fn path_metadata( + async fn archive_metadata( &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, - source_root: &Path, ) -> Result { let cache_shard = self.build_context.cache().shard( CacheBucket::BuiltWheels, @@ -572,31 +560,26 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { ); // Fetch the revision for the source distribution. - let revision = self.path_revision(source, resource, &cache_shard).await?; + let revision = self + .archive_revision(source, resource, &cache_shard) + .await?; - // From here on, scope all operations to the current build. Within the revision shard, - // there's no need to check for freshness, since entries have to be fresher than the - // revision itself. There's also no need to lock, since we never replace entries within the - // shard. + // Scope all operations to the revision. Within the revision, there's no need to check for + // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); - if self - .build_context - .cache() - .freshness(&metadata_entry, source.name()) - .is_ok_and(Freshness::is_fresh) - { - if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { - debug!("Using cached metadata for: {source}"); - return Ok(metadata); - } + if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { + debug!("Using cached metadata for: {source}"); + return Ok(metadata); } + let source_entry = cache_shard.entry("source"); + // If the backend supports `prepare_metadata_for_build_wheel`, use it. if let Some(metadata) = self - .build_metadata(source, source_root, None) + .build_metadata(source, source_entry.path(), None) .boxed() .await? { @@ -619,7 +602,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .map(|reporter| reporter.on_build_start(source)); let (_disk_filename, _filename, metadata) = self - .build_distribution(source, source_root, None, &cache_shard) + .build_distribution(source, source_entry.path(), None, &cache_shard) .await?; if let Some(task) = task { @@ -637,8 +620,184 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { Ok(metadata) } - /// Return the [`Revision`] for a local path, refreshing it if necessary. - async fn path_revision( + /// Return the [`Revision`] for a local archive, refreshing it if necessary. + async fn archive_revision( + &self, + source: &BuildableSource<'_>, + resource: &PathSourceUrl<'_>, + cache_shard: &CacheShard, + ) -> Result { + // Determine the last-modified time of the source distribution. + let modified = ArchiveTimestamp::from_file(&resource.path).map_err(Error::CacheRead)?; + + // Read the existing metadata from the cache. + let revision_entry = cache_shard.entry(REVISION); + + // If the revision already exists, return it. There's no need to check for freshness, since + // we use an exact timestamp. + if let Some(revision) = read_timestamped_revision(&revision_entry, modified)? { + return Ok(revision); + } + + // Otherwise, we need to create a new revision. + let revision = Revision::new(); + + // Unzip the archive to a temporary directory. + debug!("Unpacking source distribution: {source}"); + let entry = cache_shard.shard(revision.id()).entry("source"); + self.persist_archive(&resource.path, source, &entry).await?; + + // Persist the revision. + write_atomic( + revision_entry.path(), + rmp_serde::to_vec(&CachedByTimestamp { + timestamp: modified.timestamp(), + data: revision.clone(), + })?, + ) + .await + .map_err(Error::CacheWrite)?; + + Ok(revision) + } + + /// Build a source distribution from a local source tree (i.e., directory). + async fn source_tree( + &self, + source: &BuildableSource<'_>, + resource: &PathSourceUrl<'_>, + tags: &Tags, + ) -> Result { + let cache_shard = self.build_context.cache().shard( + CacheBucket::BuiltWheels, + WheelCache::Path(resource.url).root(), + ); + + // Fetch the revision for the source distribution. + let revision = self + .source_tree_revision(source, resource, &cache_shard) + .await?; + + // Scope all operations to the revision. Within the revision, there's no need to check for + // freshness, since entries have to be fresher than the revision itself. + let cache_shard = cache_shard.shard(revision.id()); + + // If the cache contains a compatible wheel, return it. + if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) { + return Ok(built_wheel); + } + + // Otherwise, we need to build a wheel. + let task = self + .reporter + .as_ref() + .map(|reporter| reporter.on_build_start(source)); + + let (disk_filename, filename, metadata) = self + .build_distribution(source, &resource.path, None, &cache_shard) + .await?; + + if let Some(task) = task { + if let Some(reporter) = self.reporter.as_ref() { + reporter.on_build_complete(source, task); + } + } + + // Store the metadata. + let metadata_entry = cache_shard.entry(METADATA); + write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) + .await + .map_err(Error::CacheWrite)?; + + Ok(BuiltWheelMetadata { + path: cache_shard.join(&disk_filename), + target: cache_shard.join(filename.stem()), + filename, + }) + } + + /// Build the source distribution's metadata from a local source tree (i.e., a directory). + /// + /// If the build backend supports `prepare_metadata_for_build_wheel`, this method will avoid + /// building the wheel. + async fn source_tree_metadata( + &self, + source: &BuildableSource<'_>, + resource: &PathSourceUrl<'_>, + ) -> Result { + let cache_shard = self.build_context.cache().shard( + CacheBucket::BuiltWheels, + WheelCache::Path(resource.url).root(), + ); + + // Fetch the revision for the source distribution. + let revision = self + .source_tree_revision(source, resource, &cache_shard) + .await?; + + // Scope all operations to the revision. Within the revision, there's no need to check for + // freshness, since entries have to be fresher than the revision itself. + let cache_shard = cache_shard.shard(revision.id()); + + // If the cache contains compatible metadata, return it. + let metadata_entry = cache_shard.entry(METADATA); + if self + .build_context + .cache() + .freshness(&metadata_entry, source.name()) + .is_ok_and(Freshness::is_fresh) + { + if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { + debug!("Using cached metadata for: {source}"); + return Ok(metadata); + } + } + + // If the backend supports `prepare_metadata_for_build_wheel`, use it. + if let Some(metadata) = self + .build_metadata(source, &resource.path, None) + .boxed() + .await? + { + // Store the metadata. + let cache_entry = cache_shard.entry(METADATA); + fs::create_dir_all(cache_entry.dir()) + .await + .map_err(Error::CacheWrite)?; + write_atomic(cache_entry.path(), rmp_serde::to_vec(&metadata)?) + .await + .map_err(Error::CacheWrite)?; + + return Ok(metadata); + } + + // Otherwise, we need to build a wheel. + let task = self + .reporter + .as_ref() + .map(|reporter| reporter.on_build_start(source)); + + let (_disk_filename, _filename, metadata) = self + .build_distribution(source, &resource.path, None, &cache_shard) + .await?; + + if let Some(task) = task { + if let Some(reporter) = self.reporter.as_ref() { + reporter.on_build_complete(source, task); + } + } + + // Store the metadata. + let metadata_entry = cache_shard.entry(METADATA); + write_atomic(metadata_entry.path(), rmp_serde::to_vec(&metadata)?) + .await + .map_err(Error::CacheWrite)?; + + Ok(metadata) + } + + /// Return the [`Revision`] for a local source tree, refreshing it if necessary. + async fn source_tree_revision( &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, @@ -815,21 +974,21 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } /// Download and unzip a source distribution into the cache from an HTTP response. - async fn persist_url<'data>( + async fn persist_url( &self, response: Response, source: &BuildableSource<'_>, filename: &str, - cache_entry: &'data CacheEntry, - ) -> Result<&'data Path, Error> { + cache_entry: &CacheEntry, + ) -> Result<(), Error> { let cache_path = cache_entry.path(); if cache_path.is_dir() { debug!("Distribution is already cached: {source}"); - return Ok(cache_path); + return Ok(()); } // Download and unzip the source distribution into a temporary directory. - let span = info_span!("download_source_dist", filename = filename, source_dist = %source); + let span = info_span!("persist_url", filename = filename, source_dist = %source); let temp_dir = tempfile::tempdir_in(self.build_context.cache().bucket(CacheBucket::BuiltWheels)) .map_err(Error::CacheWrite)?; @@ -855,7 +1014,49 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - Ok(cache_path) + Ok(()) + } + + /// Extract a local archive, and store it at the given [`CacheEntry`]. + async fn persist_archive( + &self, + path: &Path, + source: &BuildableSource<'_>, + cache_entry: &CacheEntry, + ) -> Result<(), Error> { + let cache_path = cache_entry.path(); + if cache_path.is_dir() { + debug!("Distribution is already cached: {source}"); + return Ok(()); + } + + debug!("Unpacking for build: {}", path.display()); + + // Unzip the archive into a temporary directory. + let temp_dir = + tempfile::tempdir_in(self.build_context.cache().bucket(CacheBucket::BuiltWheels)) + .map_err(Error::CacheWrite)?; + let reader = fs_err::tokio::File::open(&path) + .await + .map_err(Error::CacheRead)?; + uv_extract::seek::archive(reader, path, &temp_dir.path()).await?; + + // Extract the top-level directory from the archive. + let extracted = match uv_extract::strip_component(temp_dir.path()) { + Ok(top_level) => top_level, + Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.path().to_path_buf(), + Err(err) => return Err(err.into()), + }; + + // Persist it to the cache. + fs_err::tokio::create_dir_all(cache_path.parent().expect("Cache entry to have parent")) + .await + .map_err(Error::CacheWrite)?; + fs_err::tokio::rename(extracted, &cache_path) + .await + .map_err(Error::CacheWrite)?; + + Ok(()) } /// Build a source distribution, storing the built wheel in the cache. @@ -1109,29 +1310,6 @@ pub(crate) fn read_timestamped_revision( Ok(None) } -#[derive(Debug)] -enum ExtractedSource { - /// The source distribution was passed in as a directory, and so doesn't need to be extracted. - Directory(PathBuf), - /// The source distribution was passed in as an archive, and was extracted into a temporary - /// directory. - /// - /// The extracted archive and temporary directory will be deleted when the `ExtractedSource` is - /// dropped. - #[allow(dead_code)] - Archive(PathBuf, TempDir), -} - -impl ExtractedSource { - /// Return the [`Path`] to the extracted source root. - fn path(&self) -> &Path { - match self { - ExtractedSource::Directory(path) => path, - ExtractedSource::Archive(path, _) => path, - } - } -} - /// Read the [`Metadata23`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2 /// or later _and_ none of the required fields (`Requires-Python`, `Requires-Dist`, and /// `Provides-Extra`) are marked as dynamic. @@ -1236,41 +1414,3 @@ fn read_wheel_metadata( let dist_info = read_archive_metadata(filename, &mut archive)?; Ok(Metadata23::parse_metadata(&dist_info)?) } - -/// Extract a local source distribution, if it's stored as a `.tar.gz` or `.zip` archive. -/// -/// TODO(charlie): Consider storing the extracted source in the cache, to avoid re-extracting -/// on every invocation. -async fn extract_archive(path: &Path, cache: &Cache) -> Result { - let metadata = match fs::metadata(&path).await { - Ok(metadata) => metadata, - Err(err) if err.kind() == std::io::ErrorKind::NotFound => { - return Err(Error::NotFound(path.to_path_buf())); - } - Err(err) => return Err(Error::CacheRead(err)), - }; - - if metadata.is_dir() { - Ok(ExtractedSource::Directory(path.to_path_buf())) - } else { - debug!("Unpacking for build: {}", path.display()); - - let temp_dir = tempfile::tempdir_in(cache.bucket(CacheBucket::BuiltWheels)) - .map_err(Error::CacheWrite)?; - - // Unzip the archive into the temporary directory. - let reader = fs_err::tokio::File::open(&path) - .await - .map_err(Error::CacheRead)?; - uv_extract::seek::archive(reader, path, &temp_dir.path()).await?; - - // Extract the top-level directory from the archive. - let extracted = match uv_extract::strip_component(temp_dir.path()) { - Ok(top_level) => top_level, - Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.path().to_path_buf(), - Err(err) => return Err(err.into()), - }; - - Ok(ExtractedSource::Archive(extracted, temp_dir)) - } -} diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 255912a11..a874e85df 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -1601,7 +1601,7 @@ fn install_path_source_dist_cached() -> Result<()> { ----- stdout ----- ----- stderr ----- - Removed 4 files for wheel ([SIZE]) + Removed 102 files for wheel ([SIZE]) "### ); From d7ff8d93c01ac1feb9ff3a509f7620a29f46ed9f Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 9 Apr 2024 09:57:30 -0500 Subject: [PATCH 064/110] Skip scenario tests on Windows (#2932) These tests are about resolver correctness, which should not be platform dependent and Windows CI is horribly slow. --- crates/uv/tests/pip_compile_scenarios.rs | 2 +- crates/uv/tests/pip_install_scenarios.rs | 2 +- scripts/scenarios/templates/compile.mustache | 2 +- scripts/scenarios/templates/install.mustache | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index 7398d84b1..eb9c8d1e6 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -3,7 +3,7 @@ //! Generated with `./scripts/sync_scenarios.sh` //! Scenarios from //! -#![cfg(all(feature = "python", feature = "pypi"))] +#![cfg(all(feature = "python", feature = "pypi", unix))] use std::env; use std::process::Command; diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 54d3bb677..0b5ab479c 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -3,7 +3,7 @@ //! Generated with `./scripts/sync_scenarios.sh` //! Scenarios from //! -#![cfg(all(feature = "python", feature = "pypi"))] +#![cfg(all(feature = "python", feature = "pypi", unix))] use std::path::Path; use std::process::Command; diff --git a/scripts/scenarios/templates/compile.mustache b/scripts/scenarios/templates/compile.mustache index 6a21783e1..0914fc04a 100644 --- a/scripts/scenarios/templates/compile.mustache +++ b/scripts/scenarios/templates/compile.mustache @@ -3,7 +3,7 @@ //! Generated with `{{generated_with}}` //! Scenarios from <{{generated_from}}> //! -#![cfg(all(feature = "python", feature = "pypi"))] +#![cfg(all(feature = "python", feature = "pypi", unix))] use std::env; use std::process::Command; diff --git a/scripts/scenarios/templates/install.mustache b/scripts/scenarios/templates/install.mustache index 06517014f..0aa0168b7 100644 --- a/scripts/scenarios/templates/install.mustache +++ b/scripts/scenarios/templates/install.mustache @@ -3,7 +3,7 @@ //! Generated with `{{generated_with}}` //! Scenarios from <{{generated_from}}> //! -#![cfg(all(feature = "python", feature = "pypi"))] +#![cfg(all(feature = "python", feature = "pypi", unix))] use std::path::Path; use std::process::Command; From 1cdadbdec83ae226471663b189e72431ae63f39e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 9 Apr 2024 10:04:28 -0500 Subject: [PATCH 065/110] Add filtering of patch Python versions unless explicitly requested (#2930) Elides Python patch versions from the test suite unless the test specifically requests a patch version. This reduces some toil when not using our bootstrapped Python versions. Partially addresses https://github.com/astral-sh/uv/issues/2165 though we'll need changes to the scenario tests to really support their case. --- crates/uv/tests/common/mod.rs | 18 ++++++++- crates/uv/tests/pip_compile.rs | 8 ++-- crates/uv/tests/pip_compile_scenarios.rs | 16 ++++---- crates/uv/tests/pip_install.rs | 8 ++-- crates/uv/tests/pip_install_scenarios.rs | 14 +++---- crates/uv/tests/pip_sync.rs | 6 +-- crates/uv/tests/venv.rs | 50 ++++++++++++++++++------ 7 files changed, 82 insertions(+), 38 deletions(-) diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index da179ea4b..5a46a956f 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -15,10 +15,11 @@ use std::env; use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Output; +use std::str::FromStr; use uv_fs::Simplified; use uv_cache::Cache; -use uv_interpreter::find_requested_python; +use uv_interpreter::{find_requested_python, PythonVersion}; // Exclude any packages uploaded after this date. pub static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z"; @@ -69,6 +70,9 @@ impl TestContext { let site_packages = site_packages_path(&venv, format!("python{python_version}")); + let python_version = + PythonVersion::from_str(python_version).expect("Tests must use valid Python versions"); + let mut filters = Vec::new(); filters.extend( Self::path_patterns(&cache_dir) @@ -123,6 +127,18 @@ impl TestContext { // Destroy any remaining UNC prefixes (Windows only) filters.push((r"\\\\\?\\".to_string(), String::new())); + // Add Python patch version filtering unless explicitly requested to ensure + // snapshots are patch version agnostic when it is not a part of the test. + if python_version.patch().is_none() { + filters.push(( + format!( + r"({})\.\d+", + regex::escape(python_version.to_string().as_str()) + ), + "$1.[X]".to_string(), + )); + } + Self { temp_dir, cache_dir, diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index befa69456..292d4507b 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -5779,14 +5779,14 @@ requires-python = "<=3.8" let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(&format!("-e {}", editable_dir.path().display()))?; - uv_snapshot!(context.compile() + uv_snapshot!(context.filters(), context.compile() .arg("requirements.in"), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Editable `example` requires Python <=3.8, but resolution targets Python 3.12.1 + error: Editable `example` requires Python <=3.8, but resolution targets Python 3.12.[X] "### ); @@ -6056,7 +6056,7 @@ requires-python = "<=3.8" let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(&format!("example @ {}", editable_dir.path().display()))?; - uv_snapshot!(context.compile() + uv_snapshot!(context.filters(), context.compile() .arg("requirements.in"), @r###" success: false exit_code: 1 @@ -6064,7 +6064,7 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index eb9c8d1e6..222467a19 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -84,7 +84,7 @@ fn incompatible_python_compatible_override() -> Result<()> { package-a==1.0.0 ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead. + warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. Resolved 1 package in [TIME] "### ); @@ -130,7 +130,7 @@ fn compatible_python_incompatible_override() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The requested Python version 3.9 is not available; 3.11.7 will be used to build dependencies instead. + warning: The requested Python version 3.9 is not available; 3.11.[X] will be used to build dependencies instead. × No solution found when resolving dependencies: ╰─▶ Because the requested Python version (3.9) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable. @@ -184,9 +184,9 @@ fn incompatible_python_compatible_override_unavailable_no_wheels() -> Result<()> ----- stdout ----- ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead. + warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -295,9 +295,9 @@ fn incompatible_python_compatible_override_no_compatible_wheels() -> Result<()> ----- stdout ----- ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead. + warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable. "### ); @@ -353,9 +353,9 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> { ----- stdout ----- ----- stderr ----- - warning: The requested Python version 3.11 is not available; 3.9.18 will be used to build dependencies instead. + warning: The requested Python version 3.11 is not available; 3.9.[X] will be used to build dependencies instead. × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because only the following versions of package-a are available: package-a==1.0.0 package-a==2.0.0 diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 7a153913c..85a890dc1 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -2415,7 +2415,7 @@ requires-python = "<=3.8" "#, )?; - uv_snapshot!(context.install() + uv_snapshot!(context.filters(), context.install() .arg("--editable") .arg(editable_dir.path()), @r###" success: false @@ -2423,7 +2423,7 @@ requires-python = "<=3.8" ----- stdout ----- ----- stderr ----- - error: Editable `example` requires Python <=3.8, but 3.12.1 is installed + error: Editable `example` requires Python <=3.8, but 3.12.[X] is installed "### ); @@ -2864,7 +2864,7 @@ requires-python = "<=3.8" "#, )?; - uv_snapshot!(context.install() + uv_snapshot!(context.filters(), context.install() .arg(format!("example @ {}", editable_dir.path().display())), @r###" success: false exit_code: 1 @@ -2872,7 +2872,7 @@ requires-python = "<=3.8" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.8 and example==0.0.0 depends on Python<=3.8, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 0b5ab479c..0f17c4612 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -3671,7 +3671,7 @@ fn python_version_does_not_exist() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.8.18) does not satisfy Python>=3.30 and package-a==1.0.0 depends on Python>=3.30, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because the current Python version (3.8.[X]) does not satisfy Python>=3.30 and package-a==1.0.0 depends on Python>=3.30, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable. "###); @@ -3713,7 +3713,7 @@ fn python_less_than_current() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python<=3.8 and package-a==1.0.0 depends on Python<=3.8, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python<=3.8 and package-a==1.0.0 depends on Python<=3.8, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable. "###); @@ -3755,7 +3755,7 @@ fn python_greater_than_current() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. + ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10 and package-a==1.0.0 depends on Python>=3.10, we can conclude that package-a==1.0.0 cannot be used. And because you require package-a==1.0.0, we can conclude that the requirements are unsatisfiable. "###); @@ -3961,22 +3961,22 @@ fn python_greater_than_current_excluded() { ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.9.18) does not satisfy Python>=3.10,<3.11 and the current Python version (3.9.18) does not satisfy Python>=3.12, we can conclude that any of: + ╰─▶ Because the current Python version (3.9.[X]) does not satisfy Python>=3.10,<3.11 and the current Python version (3.9.[X]) does not satisfy Python>=3.12, we can conclude that any of: Python>=3.10,<3.11 Python>=3.12 are incompatible. - And because the current Python version (3.9.18) does not satisfy Python>=3.11,<3.12, we can conclude that Python>=3.10 are incompatible. + And because the current Python version (3.9.[X]) does not satisfy Python>=3.11,<3.12, we can conclude that Python>=3.10 are incompatible. And because package-a==2.0.0 depends on Python>=3.10 and only the following versions of package-a are available: package-a<=2.0.0 package-a==3.0.0 package-a==4.0.0 we can conclude that package-a>=2.0.0,<3.0.0 cannot be used. (1) - Because the current Python version (3.9.18) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9.18) does not satisfy Python>=3.12, we can conclude that Python>=3.11 are incompatible. + Because the current Python version (3.9.[X]) does not satisfy Python>=3.11,<3.12 and the current Python version (3.9.[X]) does not satisfy Python>=3.12, we can conclude that Python>=3.11 are incompatible. And because package-a==3.0.0 depends on Python>=3.11, we can conclude that package-a==3.0.0 cannot be used. And because we know from (1) that package-a>=2.0.0,<3.0.0 cannot be used, we can conclude that package-a>=2.0.0,<4.0.0 cannot be used. (2) - Because the current Python version (3.9.18) does not satisfy Python>=3.12 and package-a==4.0.0 depends on Python>=3.12, we can conclude that package-a==4.0.0 cannot be used. + Because the current Python version (3.9.[X]) does not satisfy Python>=3.12 and package-a==4.0.0 depends on Python>=3.12, we can conclude that package-a==4.0.0 cannot be used. And because we know from (2) that package-a>=2.0.0,<4.0.0 cannot be used, we can conclude that package-a>=2.0.0 cannot be used. And because you require package-a>=2.0.0, we can conclude that the requirements are unsatisfiable. "###); diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index a874e85df..b387eb9ac 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -2995,14 +2995,14 @@ requires-python = "<=3.5" let requirements_in = context.temp_dir.child("requirements.in"); requirements_in.write_str(&format!("-e {}", editable_dir.path().display()))?; - uv_snapshot!(command(&context) + uv_snapshot!(context.filters(), command(&context) .arg("requirements.in"), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Editable `example` requires Python <=3.5, but 3.12.1 is installed + error: Editable `example` requires Python <=3.5, but 3.12.[X] is installed "### ); @@ -3071,7 +3071,7 @@ requires-python = "<=3.5" ----- stderr ----- × No solution found when resolving dependencies: - ╰─▶ Because the current Python version (3.12.1) does not satisfy Python<=3.5 and example==0.0.0 depends on Python<=3.5, we can conclude that example==0.0.0 cannot be used. + ╰─▶ Because the current Python version (3.12.[X]) does not satisfy Python<=3.5 and example==0.0.0 depends on Python<=3.5, we can conclude that example==0.0.0 cannot be used. And because only example==0.0.0 is available and you require example, we can conclude that the requirements are unsatisfiable. "### ); diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/venv.rs index 1e195708e..282ad0370 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/venv.rs @@ -1,7 +1,7 @@ #![cfg(feature = "python")] -use std::ffi::OsString; use std::process::Command; +use std::{ffi::OsString, str::FromStr}; use anyhow::Result; use assert_cmd::prelude::*; @@ -9,6 +9,7 @@ use assert_fs::fixture::ChildPath; use assert_fs::prelude::*; use fs_err::PathExt; use uv_fs::Simplified; +use uv_interpreter::PythonVersion; use crate::common::{ create_bin_with_executables, get_bin, uv_snapshot, TestContext, EXCLUDE_NEWER, @@ -21,6 +22,7 @@ struct VenvTestContext { temp_dir: assert_fs::TempDir, venv: ChildPath, bin: OsString, + python_versions: Vec, } impl VenvTestContext { @@ -29,11 +31,18 @@ impl VenvTestContext { let bin = create_bin_with_executables(&temp_dir, python_versions) .expect("Failed to create bin dir"); let venv = temp_dir.child(".venv"); + let python_versions = python_versions + .iter() + .map(|version| { + PythonVersion::from_str(version).expect("Tests should use valid Python versions") + }) + .collect::>(); Self { cache_dir: assert_fs::TempDir::new().unwrap(), temp_dir, venv, bin, + python_versions, } } @@ -70,6 +79,25 @@ impl VenvTestContext { r"Activate with: (?:.*)\\Scripts\\activate".to_string(), "Activate with: source .venv/bin/activate".to_string(), )); + + // Add Python patch version filtering unless one was explicitly requested to ensure + // snapshots are patch version agnostic when it is not a part of the test. + if self + .python_versions + .iter() + .all(|version| version.patch().is_none()) + { + for python_version in &self.python_versions { + filters.push(( + format!( + r"({})\.\d+", + regex::escape(python_version.to_string().as_str()) + ), + "$1.[X]".to_string(), + )); + } + } + filters } } @@ -88,7 +116,7 @@ fn create_venv() { ----- stdout ----- ----- stderr ----- - Using Python 3.12.1 interpreter at: [PATH] + Using Python 3.12.[X] interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### @@ -107,7 +135,7 @@ fn create_venv() { ----- stdout ----- ----- stderr ----- - Using Python 3.12.1 interpreter at: [PATH] + Using Python 3.12.[X] interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### @@ -128,7 +156,7 @@ fn create_venv_defaults_to_cwd() { ----- stdout ----- ----- stderr ----- - Using Python 3.12.1 interpreter at: [PATH] + Using Python 3.12.[X] interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### @@ -151,7 +179,7 @@ fn seed() { ----- stdout ----- ----- stderr ----- - Using Python 3.12.1 interpreter at: [PATH] + Using Python 3.12.[X] interpreter at: [PATH] Creating virtualenv at: .venv + pip==24.0 Activate with: source .venv/bin/activate @@ -175,7 +203,7 @@ fn seed_older_python_version() { ----- stdout ----- ----- stderr ----- - Using Python 3.10.13 interpreter at: [PATH] + Using Python 3.10.[X] interpreter at: [PATH] Creating virtualenv at: .venv + pip==24.0 + setuptools==69.2.0 @@ -293,7 +321,7 @@ fn file_exists() -> Result<()> { ----- stdout ----- ----- stderr ----- - Using Python 3.12.1 interpreter at: [PATH] + Using Python 3.12.[X] interpreter at: [PATH] Creating virtualenv at: .venv uv::venv::creation @@ -321,7 +349,7 @@ fn empty_dir_exists() -> Result<()> { ----- stdout ----- ----- stderr ----- - Using Python 3.12.1 interpreter at: [PATH] + Using Python 3.12.[X] interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### @@ -350,7 +378,7 @@ fn non_empty_dir_exists() -> Result<()> { ----- stdout ----- ----- stderr ----- - Using Python 3.12.1 interpreter at: [PATH] + Using Python 3.12.[X] interpreter at: [PATH] Creating virtualenv at: .venv uv::venv::creation @@ -395,7 +423,7 @@ fn windows_shims() -> Result<()> { ----- stderr ----- warning: virtualenv's `--clear` has no effect (uv always clears the virtual environment). - Using Python 3.8.12 interpreter at: [PATH] + Using Python 3.8.[X] interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### @@ -422,7 +450,7 @@ fn virtualenv_compatibility() { ----- stderr ----- warning: virtualenv's `--clear` has no effect (uv always clears the virtual environment). - Using Python 3.12.1 interpreter at: [PATH] + Using Python 3.12.[X] interpreter at: [PATH] Creating virtualenv at: .venv Activate with: source .venv/bin/activate "### From a4f5a7d23344ba0268206b3310fb216c9d817e74 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 9 Apr 2024 12:06:11 -0400 Subject: [PATCH 066/110] Bump version to v0.1.30 (#2934) --- CHANGELOG.md | 21 +++++++++++++++++++++ Cargo.lock | 4 ++-- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- pyproject.toml | 2 +- 5 files changed, 26 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2457a2133..634a217eb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,26 @@ # Changelog +## 0.1.30 + +### Enhancements + +- Show resolution diagnostics after `pip install` ([#2829](https://github.com/astral-sh/uv/pull/2829)) + +### Performance + +- Speed up cold-cache `urllib3`-`boto3`-`botocore` performance with batched prefetching ([#2452](https://github.com/astral-sh/uv/pull/2452)) + +### Bug fixes + +- Backtrack on distributions with invalid metadata ([#2834](https://github.com/astral-sh/uv/pull/2834)) +- Include LICENSE files in source distribution ([#2855](https://github.com/astral-sh/uv/pull/2855)) +- Respect `--no-build` and `--no-binary` in `--find-links` ([#2826](https://github.com/astral-sh/uv/pull/2826)) +- Respect cached local `--find-links` in install plan ([#2907](https://github.com/astral-sh/uv/pull/2907)) +- Avoid panic with multiple confirmation handlers ([#2903](https://github.com/astral-sh/uv/pull/2903)) +- Use scheme parsing to determine absolute vs. relative URLs ([#2904](https://github.com/astral-sh/uv/pull/2904)) +- Remove additional 'because' in resolution failure messages ([#2849](https://github.com/astral-sh/uv/pull/2849)) +- Use `miette` when printing `pip sync` resolution failures ([#2848](https://github.com/astral-sh/uv/pull/2848)) + ## 0.1.29 ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index c8bc8a192..1f6272a44 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,7 +4280,7 @@ checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" [[package]] name = "uv" -version = "0.1.29" +version = "0.1.30" dependencies = [ "anstream", "anyhow", @@ -4798,7 +4798,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.1.29" +version = "0.1.30" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 49f8155d6..6f35ba3aa 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.1.29" +version = "0.1.30" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 3aca2b61b..a9f4d76be 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.1.29" +version = "0.1.30" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/pyproject.toml b/pyproject.toml index e1d38c54e..7ef559e57 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.1.29" +version = "0.1.30" description = "An extremely fast Python package installer and resolver, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From 90735660cb36fb26b0361efbff6e8c8b10d7636d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 9 Apr 2024 12:19:22 -0400 Subject: [PATCH 067/110] Upgrade cargo-dist (#2936) --- .github/workflows/release.yml | 12 ++++++------ Cargo.toml | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 3fec1e889..289b7a1c4 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -65,7 +65,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/axodotdev/cargo-dist/releases/download/v0.13.0-prerelease.3/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.0/cargo-dist-installer.sh | sh" # sure would be cool if github gave us proper conditionals... # so here's a doubly-nested ternary-via-truthiness to try to provide the best possible # functionality based on whether this is a pull_request, and whether it's from a fork. @@ -97,7 +97,7 @@ jobs: needs: - plan - custom-build-binaries - runs-on: "ubuntu-22.04" + runs-on: "ubuntu-20.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} BUILD_MANIFEST_NAME: target/distrib/global-dist-manifest.json @@ -107,7 +107,7 @@ jobs: submodules: recursive - name: Install cargo-dist shell: bash - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.0-prerelease.3/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.0/cargo-dist-installer.sh | sh" # Get all the local artifacts for the global tasks to use (for e.g. checksums) - name: Fetch local artifacts uses: actions/download-artifact@v4 @@ -143,7 +143,7 @@ jobs: if: ${{ always() && needs.plan.outputs.publishing == 'true' && (needs.build-global-artifacts.result == 'skipped' || needs.build-global-artifacts.result == 'success') }} env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - runs-on: "ubuntu-22.04" + runs-on: "ubuntu-20.04" outputs: val: ${{ steps.host.outputs.manifest }} steps: @@ -151,7 +151,7 @@ jobs: with: submodules: recursive - name: Install cargo-dist - run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.0-prerelease.3/cargo-dist-installer.sh | sh" + run: "curl --proto '=https' --tlsv1.2 -LsSf https://github.com/axodotdev/cargo-dist/releases/download/v0.13.0/cargo-dist-installer.sh | sh" # Fetch artifacts from scratch-storage - name: Fetch artifacts uses: actions/download-artifact@v4 @@ -198,7 +198,7 @@ jobs: # still allowing individual publish jobs to skip themselves (for prereleases). # "host" however must run to completion, no skipping allowed! if: ${{ always() && needs.host.result == 'success' && (needs.custom-publish-pypi.result == 'skipped' || needs.custom-publish-pypi.result == 'success') }} - runs-on: "ubuntu-22.04" + runs-on: "ubuntu-20.04" env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} steps: diff --git a/Cargo.toml b/Cargo.toml index b4feb886a..c70896fab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -197,7 +197,7 @@ lto = "thin" # Config for 'cargo dist' [workspace.metadata.dist] # The preferred cargo-dist version to use in CI (Cargo.toml SemVer syntax) -cargo-dist-version = "0.13.0-prerelease.3" +cargo-dist-version = "0.13.0" # CI backends to support ci = ["github"] # The installers to generate for each app From 1512e07a2e7c05b7c9fcb457f35c7ef0f25d8d9b Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 9 Apr 2024 11:35:53 -0500 Subject: [PATCH 068/110] Split configuration options out of `uv-types` (#2924) Needed to prevent circular dependencies in my toolchain work (#2931). I think this is probably a reasonable change as we move towards persistent configuration too? Unfortunately `BuildIsolation` needs to be in `uv-types` to avoid circular dependencies still. We might be able to resolve that in the future. --- Cargo.lock | 30 +++++++++++++++++-- Cargo.toml | 1 + crates/requirements-txt/Cargo.toml | 2 +- crates/requirements-txt/src/lib.rs | 2 +- crates/uv-build/Cargo.toml | 1 + crates/uv-build/src/lib.rs | 5 ++-- crates/uv-client/Cargo.toml | 2 +- crates/uv-client/src/flat_index.rs | 2 +- crates/uv-client/src/registry_client.rs | 2 +- crates/uv-configuration/Cargo.toml | 30 +++++++++++++++++++ .../src/build_options.rs | 15 ---------- .../src/config_settings.rs | 0 .../src/constraints.rs | 0 crates/uv-configuration/src/lib.rs | 13 ++++++++ .../src/name_specifiers.rs | 0 .../src/overrides.rs | 0 .../src/package_options.rs | 0 crates/uv-dev/Cargo.toml | 1 + crates/uv-dev/src/build.rs | 6 ++-- crates/uv-dev/src/resolve_cli.rs | 3 +- crates/uv-dev/src/resolve_many.rs | 4 +-- crates/uv-dispatch/Cargo.toml | 2 +- crates/uv-dispatch/src/lib.rs | 6 ++-- crates/uv-distribution/Cargo.toml | 2 +- .../src/distribution_database.rs | 3 +- crates/uv-distribution/src/source/mod.rs | 3 +- crates/uv-installer/Cargo.toml | 1 + crates/uv-installer/src/plan.rs | 2 +- crates/uv-requirements/Cargo.toml | 1 + crates/uv-requirements/src/lookahead.rs | 3 +- crates/uv-requirements/src/specification.rs | 2 +- crates/uv-requirements/src/upgrade.rs | 2 +- crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/exclusions.rs | 2 +- crates/uv-resolver/src/manifest.rs | 3 +- .../uv-resolver/src/pubgrub/dependencies.rs | 2 +- crates/uv-resolver/src/resolver/mod.rs | 3 +- crates/uv-resolver/src/resolver/provider.rs | 3 +- crates/uv-resolver/src/version_map.rs | 2 +- crates/uv-resolver/tests/resolver.rs | 6 ++-- crates/uv-types/Cargo.toml | 1 + crates/uv-types/src/builds.rs | 15 ++++++++++ crates/uv-types/src/lib.rs | 14 ++------- crates/uv-types/src/traits.rs | 3 +- crates/uv/Cargo.toml | 1 + crates/uv/src/commands/pip_compile.rs | 9 +++--- crates/uv/src/commands/pip_install.rs | 9 +++--- crates/uv/src/commands/pip_sync.rs | 8 ++--- crates/uv/src/commands/venv.rs | 6 ++-- crates/uv/src/main.rs | 10 +++---- 50 files changed, 155 insertions(+), 89 deletions(-) create mode 100644 crates/uv-configuration/Cargo.toml rename crates/{uv-types => uv-configuration}/src/build_options.rs (96%) rename crates/{uv-types => uv-configuration}/src/config_settings.rs (100%) rename crates/{uv-types => uv-configuration}/src/constraints.rs (100%) create mode 100644 crates/uv-configuration/src/lib.rs rename crates/{uv-types => uv-configuration}/src/name_specifiers.rs (100%) rename crates/{uv-types => uv-configuration}/src/overrides.rs (100%) rename crates/{uv-types => uv-configuration}/src/package_options.rs (100%) create mode 100644 crates/uv-types/src/builds.rs diff --git a/Cargo.lock b/Cargo.lock index 1f6272a44..244e925c6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2952,9 +2952,9 @@ dependencies = [ "unscanny", "url", "uv-client", + "uv-configuration", "uv-fs", "uv-normalize", - "uv-types", "uv-warnings", ] @@ -4328,6 +4328,7 @@ dependencies = [ "uv-auth", "uv-cache", "uv-client", + "uv-configuration", "uv-dispatch", "uv-distribution", "uv-fs", @@ -4384,6 +4385,7 @@ dependencies = [ "tokio", "toml", "tracing", + "uv-configuration", "uv-fs", "uv-interpreter", "uv-types", @@ -4458,14 +4460,30 @@ dependencies = [ "urlencoding", "uv-auth", "uv-cache", + "uv-configuration", "uv-fs", "uv-normalize", - "uv-types", "uv-version", "uv-warnings", "webpki-roots", ] +[[package]] +name = "uv-configuration" +version = "0.0.1" +dependencies = [ + "anyhow", + "clap", + "distribution-types", + "itertools 0.12.1", + "pep508_rs", + "rustc-hash", + "serde", + "serde_json", + "uv-cache", + "uv-normalize", +] + [[package]] name = "uv-dev" version = "0.0.1" @@ -4500,6 +4518,7 @@ dependencies = [ "uv-build", "uv-cache", "uv-client", + "uv-configuration", "uv-dispatch", "uv-installer", "uv-interpreter", @@ -4523,9 +4542,9 @@ dependencies = [ "uv-build", "uv-cache", "uv-client", + "uv-configuration", "uv-installer", "uv-interpreter", - "uv-requirements", "uv-resolver", "uv-types", ] @@ -4560,6 +4579,7 @@ dependencies = [ "url", "uv-cache", "uv-client", + "uv-configuration", "uv-extract", "uv-fs", "uv-git", @@ -4652,6 +4672,7 @@ dependencies = [ "url", "uv-cache", "uv-client", + "uv-configuration", "uv-distribution", "uv-extract", "uv-fs", @@ -4725,6 +4746,7 @@ dependencies = [ "tracing", "url", "uv-client", + "uv-configuration", "uv-distribution", "uv-fs", "uv-normalize", @@ -4771,6 +4793,7 @@ dependencies = [ "url", "uv-cache", "uv-client", + "uv-configuration", "uv-distribution", "uv-interpreter", "uv-normalize", @@ -4792,6 +4815,7 @@ dependencies = [ "serde", "serde_json", "uv-cache", + "uv-configuration", "uv-interpreter", "uv-normalize", ] diff --git a/Cargo.toml b/Cargo.toml index c70896fab..5e4a53df1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -44,6 +44,7 @@ uv-normalize = { path = "crates/uv-normalize" } uv-requirements = { path = "crates/uv-requirements" } uv-resolver = { path = "crates/uv-resolver" } uv-types = { path = "crates/uv-types" } +uv-configuration = { path = "crates/uv-configuration" } uv-trampoline = { path = "crates/uv-trampoline" } uv-version = { path = "crates/uv-version" } uv-virtualenv = { path = "crates/uv-virtualenv" } diff --git a/crates/requirements-txt/Cargo.toml b/crates/requirements-txt/Cargo.toml index 17c7525b6..0d91710db 100644 --- a/crates/requirements-txt/Cargo.toml +++ b/crates/requirements-txt/Cargo.toml @@ -17,7 +17,7 @@ pep508_rs = { workspace = true, features = ["rkyv", "serde", "non-pep508-extensi uv-client = { workspace = true } uv-fs = { workspace = true } uv-normalize = { workspace = true } -uv-types = { workspace = true } +uv-configuration = { workspace = true } uv-warnings = { workspace = true } fs-err = { workspace = true } diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 47968a3e3..17732dc12 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -52,9 +52,9 @@ use pep508_rs::{ #[cfg(feature = "http")] use uv_client::BaseClient; use uv_client::BaseClientBuilder; +use uv_configuration::{NoBinary, NoBuild, PackageNameSpecifier}; use uv_fs::{normalize_url_path, Simplified}; use uv_normalize::ExtraName; -use uv_types::{NoBinary, NoBuild, PackageNameSpecifier}; use uv_warnings::warn_user; /// We emit one of those for each requirements.txt entry diff --git a/crates/uv-build/Cargo.toml b/crates/uv-build/Cargo.toml index 1bfb137d7..2d8f70278 100644 --- a/crates/uv-build/Cargo.toml +++ b/crates/uv-build/Cargo.toml @@ -20,6 +20,7 @@ pep508_rs = { workspace = true } uv-fs = { workspace = true } uv-interpreter = { workspace = true } uv-types = { workspace = true, features = ["serde"] } +uv-configuration = { workspace = true, features = ["serde"] } uv-virtualenv = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index 0606c371f..6c914de66 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -28,11 +28,10 @@ use tracing::{debug, info_span, instrument, Instrument}; use distribution_types::Resolution; use pep440_rs::Version; use pep508_rs::{PackageName, Requirement}; +use uv_configuration::{BuildKind, ConfigSettings, SetupPyStrategy}; use uv_fs::{PythonExt, Simplified}; use uv_interpreter::{Interpreter, PythonEnvironment}; -use uv_types::{ - BuildContext, BuildIsolation, BuildKind, ConfigSettings, SetupPyStrategy, SourceBuildTrait, -}; +use uv_types::{BuildContext, BuildIsolation, SourceBuildTrait}; /// e.g. `pygraphviz/graphviz_wrap.c:3020:10: fatal error: graphviz/cgraph.h: No such file or directory` static MISSING_HEADER_RE: Lazy = Lazy::new(|| { diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 139c03983..bddd71163 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -15,7 +15,7 @@ uv-auth = { workspace = true } uv-cache = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-normalize = { workspace = true } -uv-types = { workspace = true } +uv-configuration = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } pypi-types = { workspace = true } diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index 45eac5af8..d44b28464 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -19,8 +19,8 @@ use pep508_rs::VerbatimUrl; use platform_tags::Tags; use pypi_types::Hashes; use uv_cache::{Cache, CacheBucket}; +use uv_configuration::{NoBinary, NoBuild}; use uv_normalize::PackageName; -use uv_types::{NoBinary, NoBuild}; use crate::cached_client::{CacheControl, CachedClientError}; use crate::html::SimpleHtml; diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index cf2b23771..2b740e745 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -22,8 +22,8 @@ use platform_tags::Platform; use pypi_types::{Metadata23, SimpleJson}; use uv_auth::KeyringProvider; use uv_cache::{Cache, CacheBucket, WheelCache}; +use uv_configuration::IndexStrategy; use uv_normalize::PackageName; -use uv_types::IndexStrategy; use crate::base_client::{BaseClient, BaseClientBuilder}; use crate::cached_client::CacheControl; diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml new file mode 100644 index 000000000..67524ef8d --- /dev/null +++ b/crates/uv-configuration/Cargo.toml @@ -0,0 +1,30 @@ +[package] +name = "uv-configuration" +version = "0.0.1" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] +distribution-types = { workspace = true } +pep508_rs = { workspace = true } +uv-cache = { workspace = true } +uv-normalize = { workspace = true } + +anyhow = { workspace = true } +clap = { workspace = true, features = ["derive"], optional = true } +itertools = { workspace = true } +rustc-hash = { workspace = true } +serde = { workspace = true, optional = true } +serde_json = { workspace = true, optional = true } + +[features] +default = [] +serde = ["dep:serde", "dep:serde_json"] diff --git a/crates/uv-types/src/build_options.rs b/crates/uv-configuration/src/build_options.rs similarity index 96% rename from crates/uv-types/src/build_options.rs rename to crates/uv-configuration/src/build_options.rs index bcdaee92b..14c8677a7 100644 --- a/crates/uv-types/src/build_options.rs +++ b/crates/uv-configuration/src/build_options.rs @@ -1,24 +1,9 @@ use std::fmt::{Display, Formatter}; use pep508_rs::PackageName; -use uv_interpreter::PythonEnvironment; use crate::{PackageNameSpecifier, PackageNameSpecifiers}; -/// Whether to enforce build isolation when building source distributions. -#[derive(Debug, Copy, Clone)] -pub enum BuildIsolation<'a> { - Isolated, - Shared(&'a PythonEnvironment), -} - -impl<'a> BuildIsolation<'a> { - /// Returns `true` if build isolation is enforced. - pub fn is_isolated(&self) -> bool { - matches!(self, Self::Isolated) - } -} - /// The strategy to use when building source distributions that lack a `pyproject.toml`. #[derive(Copy, Clone, Debug, Default, PartialEq, Eq)] pub enum SetupPyStrategy { diff --git a/crates/uv-types/src/config_settings.rs b/crates/uv-configuration/src/config_settings.rs similarity index 100% rename from crates/uv-types/src/config_settings.rs rename to crates/uv-configuration/src/config_settings.rs diff --git a/crates/uv-types/src/constraints.rs b/crates/uv-configuration/src/constraints.rs similarity index 100% rename from crates/uv-types/src/constraints.rs rename to crates/uv-configuration/src/constraints.rs diff --git a/crates/uv-configuration/src/lib.rs b/crates/uv-configuration/src/lib.rs new file mode 100644 index 000000000..665568f2b --- /dev/null +++ b/crates/uv-configuration/src/lib.rs @@ -0,0 +1,13 @@ +pub use build_options::*; +pub use config_settings::*; +pub use constraints::*; +pub use name_specifiers::*; +pub use overrides::*; +pub use package_options::*; + +mod build_options; +mod config_settings; +mod constraints; +mod name_specifiers; +mod overrides; +mod package_options; diff --git a/crates/uv-types/src/name_specifiers.rs b/crates/uv-configuration/src/name_specifiers.rs similarity index 100% rename from crates/uv-types/src/name_specifiers.rs rename to crates/uv-configuration/src/name_specifiers.rs diff --git a/crates/uv-types/src/overrides.rs b/crates/uv-configuration/src/overrides.rs similarity index 100% rename from crates/uv-types/src/overrides.rs rename to crates/uv-configuration/src/overrides.rs diff --git a/crates/uv-types/src/package_options.rs b/crates/uv-configuration/src/package_options.rs similarity index 100% rename from crates/uv-types/src/package_options.rs rename to crates/uv-configuration/src/package_options.rs diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index 7ad14c089..764bdd3fc 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -29,6 +29,7 @@ uv-interpreter = { workspace = true } uv-normalize = { workspace = true } uv-resolver = { workspace = true } uv-types = { workspace = true } +uv-configuration = { workspace = true } # Any dependencies that are exclusively used in `uv-dev` should be listed as non-workspace # dependencies, to ensure that we're forced to think twice before including them in other crates. diff --git a/crates/uv-dev/src/build.rs b/crates/uv-dev/src/build.rs index 0a49a0883..20430e1a0 100644 --- a/crates/uv-dev/src/build.rs +++ b/crates/uv-dev/src/build.rs @@ -10,13 +10,11 @@ use rustc_hash::FxHashMap; use uv_build::{SourceBuild, SourceBuildContext}; use uv_cache::{Cache, CacheArgs}; use uv_client::{FlatIndex, RegistryClientBuilder}; +use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_interpreter::PythonEnvironment; use uv_resolver::InMemoryIndex; -use uv_types::NoBinary; -use uv_types::{ - BuildContext, BuildIsolation, BuildKind, ConfigSettings, InFlight, NoBuild, SetupPyStrategy, -}; +use uv_types::{BuildContext, BuildIsolation, InFlight}; #[derive(Parser)] pub(crate) struct BuildArgs { diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index 3d9bb3cc7..27aa0748b 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -13,11 +13,12 @@ use distribution_types::{FlatIndexLocation, IndexLocations, IndexUrl, Resolution use pep508_rs::Requirement; use uv_cache::{Cache, CacheArgs}; use uv_client::{FlatIndex, FlatIndexClient, RegistryClientBuilder}; +use uv_configuration::{ConfigSettings, NoBinary, NoBuild, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_installer::SitePackages; use uv_interpreter::PythonEnvironment; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; -use uv_types::{BuildIsolation, ConfigSettings, InFlight, NoBinary, NoBuild, SetupPyStrategy}; +use uv_types::{BuildIsolation, InFlight}; #[derive(ValueEnum, Default, Clone)] pub(crate) enum ResolveCliFormat { diff --git a/crates/uv-dev/src/resolve_many.rs b/crates/uv-dev/src/resolve_many.rs index bd0e53b1c..07f1ddc18 100644 --- a/crates/uv-dev/src/resolve_many.rs +++ b/crates/uv-dev/src/resolve_many.rs @@ -15,12 +15,12 @@ use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; use pep508_rs::{Requirement, VersionOrUrl}; use uv_cache::{Cache, CacheArgs}; use uv_client::{FlatIndex, OwnedArchive, RegistryClient, RegistryClientBuilder}; +use uv_configuration::{ConfigSettings, NoBinary, NoBuild, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; use uv_resolver::InMemoryIndex; -use uv_types::NoBinary; -use uv_types::{BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBuild, SetupPyStrategy}; +use uv_types::{BuildContext, BuildIsolation, InFlight}; #[derive(Parser)] pub(crate) struct ResolveManyArgs { diff --git a/crates/uv-dispatch/Cargo.toml b/crates/uv-dispatch/Cargo.toml index 727e11668..7cd8476d4 100644 --- a/crates/uv-dispatch/Cargo.toml +++ b/crates/uv-dispatch/Cargo.toml @@ -21,9 +21,9 @@ uv-cache = { workspace = true } uv-client = { workspace = true } uv-installer = { workspace = true } uv-interpreter = { workspace = true } -uv-requirements = { workspace = true } uv-resolver = { workspace = true } uv-types = { workspace = true } +uv-configuration = { workspace = true } anyhow = { workspace = true } futures = { workspace = true } diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 7a73c81d4..52f1cdda2 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -17,13 +17,11 @@ use pep508_rs::Requirement; use uv_build::{SourceBuild, SourceBuildContext}; use uv_cache::Cache; use uv_client::{FlatIndex, RegistryClient}; +use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, Reinstall, SetupPyStrategy}; use uv_installer::{Downloader, Installer, Plan, Planner, SitePackages}; use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; -use uv_types::{ - BuildContext, BuildIsolation, BuildKind, ConfigSettings, EmptyInstalledPackages, InFlight, - NoBinary, NoBuild, Reinstall, SetupPyStrategy, -}; +use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, InFlight}; /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] /// documentation. diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 42b8c9e12..53d244433 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -28,6 +28,7 @@ uv-fs = { workspace = true, features = ["tokio"] } uv-git = { workspace = true, features = ["vendored-openssl"] } uv-normalize = { workspace = true } uv-types = { workspace = true } +uv-configuration = { workspace = true } anyhow = { workspace = true } fs-err = { workspace = true } @@ -46,4 +47,3 @@ tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } url = { workspace = true } zip = { workspace = true } - diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 477a408b1..6ece58e51 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -17,8 +17,9 @@ use platform_tags::Tags; use pypi_types::Metadata23; use uv_cache::{ArchiveTimestamp, CacheBucket, CacheEntry, CachedByTimestamp, WheelCache}; use uv_client::{CacheControl, CachedClientError, Connectivity, RegistryClient}; +use uv_configuration::{NoBinary, NoBuild}; use uv_fs::write_atomic; -use uv_types::{BuildContext, NoBinary, NoBuild}; +use uv_types::BuildContext; use crate::locks::Locks; use crate::{Error, LocalWheel, Reporter, SourceDistributionBuilder}; diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 59c86adb3..34b52c105 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -28,8 +28,9 @@ use uv_cache::{ use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; +use uv_configuration::{BuildKind, NoBuild}; use uv_fs::write_atomic; -use uv_types::{BuildContext, BuildKind, NoBuild, SourceBuildTrait}; +use uv_types::{BuildContext, SourceBuildTrait}; use crate::error::Error; use crate::git::{fetch_git_archive, resolve_precise}; diff --git a/crates/uv-installer/Cargo.toml b/crates/uv-installer/Cargo.toml index efb269cc2..413664f62 100644 --- a/crates/uv-installer/Cargo.toml +++ b/crates/uv-installer/Cargo.toml @@ -29,6 +29,7 @@ uv-interpreter = { workspace = true } uv-normalize = { workspace = true } uv-types = { workspace = true } uv-warnings = { workspace = true } +uv-configuration = { workspace = true } anyhow = { workspace = true } async-channel = { workspace = true } diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index d6ef6668c..985a6f732 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -13,10 +13,10 @@ use distribution_types::{ use pep508_rs::{Requirement, VersionOrUrl}; use platform_tags::Tags; use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache, CacheBucket, WheelCache}; +use uv_configuration::{NoBinary, Reinstall}; use uv_distribution::{BuiltWheelIndex, RegistryWheelIndex}; use uv_fs::Simplified; use uv_interpreter::PythonEnvironment; -use uv_types::{NoBinary, Reinstall}; use crate::{ResolvedEditable, SitePackages}; diff --git a/crates/uv-requirements/Cargo.toml b/crates/uv-requirements/Cargo.toml index c2befe770..658a89479 100644 --- a/crates/uv-requirements/Cargo.toml +++ b/crates/uv-requirements/Cargo.toml @@ -23,6 +23,7 @@ uv-normalize = { workspace = true } uv-resolver = { workspace = true, features = ["clap"] } uv-types = { workspace = true } uv-warnings = { workspace = true } +uv-configuration = { workspace = true } anyhow = { workspace = true } configparser = { workspace = true } diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index 685b475ab..d2c0131fc 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -10,9 +10,10 @@ use distribution_types::{Dist, DistributionMetadata, LocalEditable}; use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use pypi_types::Metadata23; use uv_client::RegistryClient; +use uv_configuration::{Constraints, Overrides}; use uv_distribution::{DistributionDatabase, Reporter}; use uv_resolver::{InMemoryIndex, MetadataResponse}; -use uv_types::{BuildContext, Constraints, Overrides, RequestedRequirements}; +use uv_types::{BuildContext, RequestedRequirements}; /// A resolver for resolving lookahead requirements from direct URLs. /// diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index eb73de073..a952e5473 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -9,9 +9,9 @@ use distribution_types::{FlatIndexLocation, IndexUrl}; use pep508_rs::{Requirement, RequirementsTxtRequirement}; use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt}; use uv_client::BaseClientBuilder; +use uv_configuration::{NoBinary, NoBuild}; use uv_fs::Simplified; use uv_normalize::{ExtraName, PackageName}; -use uv_types::{NoBinary, NoBuild}; use crate::pyproject::{Pep621Metadata, PyProjectToml}; use crate::{ExtrasSpecification, RequirementsSource}; diff --git a/crates/uv-requirements/src/upgrade.rs b/crates/uv-requirements/src/upgrade.rs index 49a1cc1e3..46c22ca0e 100644 --- a/crates/uv-requirements/src/upgrade.rs +++ b/crates/uv-requirements/src/upgrade.rs @@ -4,8 +4,8 @@ use anyhow::Result; use requirements_txt::RequirementsTxt; use uv_client::{BaseClientBuilder, Connectivity}; +use uv_configuration::Upgrade; use uv_resolver::{Preference, PreferenceError}; -use uv_types::Upgrade; /// Load the preferred requirements from an existing lockfile, applying the upgrade strategy. pub async fn read_lockfile( diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index ece532d2b..1c8c5dbb3 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -30,6 +30,7 @@ uv-interpreter = { workspace = true } uv-normalize = { workspace = true } uv-types = { workspace = true } uv-warnings = { workspace = true } +uv-configuration = { workspace = true } anstream = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-resolver/src/exclusions.rs b/crates/uv-resolver/src/exclusions.rs index e7c6ade4d..34593af9d 100644 --- a/crates/uv-resolver/src/exclusions.rs +++ b/crates/uv-resolver/src/exclusions.rs @@ -1,6 +1,6 @@ use pep508_rs::PackageName; use rustc_hash::FxHashSet; -use uv_types::{Reinstall, Upgrade}; +use uv_configuration::{Reinstall, Upgrade}; /// Tracks locally installed packages that should not be selected during resolution. #[derive(Debug, Default, Clone)] diff --git a/crates/uv-resolver/src/manifest.rs b/crates/uv-resolver/src/manifest.rs index e6b6607f6..2801aef1b 100644 --- a/crates/uv-resolver/src/manifest.rs +++ b/crates/uv-resolver/src/manifest.rs @@ -1,8 +1,9 @@ use distribution_types::LocalEditable; use pep508_rs::{MarkerEnvironment, Requirement}; use pypi_types::Metadata23; +use uv_configuration::{Constraints, Overrides}; use uv_normalize::PackageName; -use uv_types::{Constraints, Overrides, RequestedRequirements}; +use uv_types::RequestedRequirements; use crate::{preferences::Preference, Exclusions}; diff --git a/crates/uv-resolver/src/pubgrub/dependencies.rs b/crates/uv-resolver/src/pubgrub/dependencies.rs index 28760cee5..6920fb0c1 100644 --- a/crates/uv-resolver/src/pubgrub/dependencies.rs +++ b/crates/uv-resolver/src/pubgrub/dependencies.rs @@ -5,8 +5,8 @@ use tracing::warn; use distribution_types::Verbatim; use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; +use uv_configuration::{Constraints, Overrides}; use uv_normalize::{ExtraName, PackageName}; -use uv_types::{Constraints, Overrides}; use crate::pubgrub::specifier::PubGrubSpecifier; use crate::pubgrub::PubGrubPackage; diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 6fb72512b..11ec5a9b0 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -27,10 +27,11 @@ use platform_tags::Tags; use pypi_types::Metadata23; pub(crate) use urls::Urls; use uv_client::{FlatIndex, RegistryClient}; +use uv_configuration::{Constraints, Overrides}; use uv_distribution::DistributionDatabase; use uv_interpreter::Interpreter; use uv_normalize::PackageName; -use uv_types::{BuildContext, Constraints, InstalledPackagesProvider, Overrides}; +use uv_types::{BuildContext, InstalledPackagesProvider}; use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::editables::Editables; diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index af89f5820..10c89a001 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -7,9 +7,10 @@ use distribution_types::{Dist, IndexLocations}; use platform_tags::Tags; use pypi_types::Metadata23; use uv_client::{FlatIndex, RegistryClient}; +use uv_configuration::{NoBinary, NoBuild}; use uv_distribution::DistributionDatabase; use uv_normalize::PackageName; -use uv_types::{BuildContext, NoBinary, NoBuild}; +use uv_types::BuildContext; use crate::python_requirement::PythonRequirement; use crate::version_map::VersionMap; diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 3d0de4189..4eb5f5285 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -15,8 +15,8 @@ use platform_tags::Tags; use pypi_types::{Hashes, Yanked}; use rkyv::{de::deserializers::SharedDeserializeMap, Deserialize}; use uv_client::{FlatDistributions, OwnedArchive, SimpleMetadata, VersionFiles}; +use uv_configuration::{NoBinary, NoBuild}; use uv_normalize::PackageName; -use uv_types::{NoBinary, NoBuild}; use uv_warnings::warn_user_once; use crate::{python_requirement::PythonRequirement, yanks::AllowedYanks}; diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 3fc94d237..69734adbb 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -15,15 +15,13 @@ use pep508_rs::{MarkerEnvironment, Requirement, StringVersion}; use platform_tags::{Arch, Os, Platform, Tags}; use uv_cache::Cache; use uv_client::{FlatIndex, RegistryClientBuilder}; +use uv_configuration::{BuildKind, Constraints, NoBinary, NoBuild, Overrides, SetupPyStrategy}; use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment}; use uv_resolver::{ DisplayResolutionGraph, Exclusions, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_types::{ - BuildContext, BuildIsolation, BuildKind, Constraints, EmptyInstalledPackages, NoBinary, - NoBuild, Overrides, SetupPyStrategy, SourceBuildTrait, -}; +use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, SourceBuildTrait}; // Exclude any packages uploaded after this date. static EXCLUDE_NEWER: Lazy> = Lazy::new(|| { diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index 4fe6ccb29..a4f22dd51 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -19,6 +19,7 @@ pep508_rs = { workspace = true } uv-cache = { workspace = true } uv-interpreter = { workspace = true } uv-normalize = { workspace = true } +uv-configuration = { workspace = true } anyhow = { workspace = true } clap = { workspace = true, features = ["derive"], optional = true } diff --git a/crates/uv-types/src/builds.rs b/crates/uv-types/src/builds.rs new file mode 100644 index 000000000..9cc80f7e2 --- /dev/null +++ b/crates/uv-types/src/builds.rs @@ -0,0 +1,15 @@ +use uv_interpreter::PythonEnvironment; + +/// Whether to enforce build isolation when building source distributions. +#[derive(Debug, Copy, Clone)] +pub enum BuildIsolation<'a> { + Isolated, + Shared(&'a PythonEnvironment), +} + +impl<'a> BuildIsolation<'a> { + /// Returns `true` if build isolation is enforced. + pub fn is_isolated(&self) -> bool { + matches!(self, Self::Isolated) + } +} diff --git a/crates/uv-types/src/lib.rs b/crates/uv-types/src/lib.rs index 40a0edc0a..cf28981fd 100644 --- a/crates/uv-types/src/lib.rs +++ b/crates/uv-types/src/lib.rs @@ -1,20 +1,10 @@ //! Fundamental types shared across `uv` crates. -pub use build_options::*; -pub use config_settings::*; -pub use constraints::*; +pub use builds::*; pub use downloads::*; -pub use name_specifiers::*; -pub use overrides::*; -pub use package_options::*; pub use requirements::*; pub use traits::*; -mod build_options; -mod config_settings; -mod constraints; +mod builds; mod downloads; -mod name_specifiers; -mod overrides; -mod package_options; mod requirements; mod traits; diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index 7370d2c5c..62b2ea818 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -9,7 +9,8 @@ use pep508_rs::{PackageName, Requirement}; use uv_cache::Cache; use uv_interpreter::{Interpreter, PythonEnvironment}; -use crate::{BuildIsolation, BuildKind, NoBinary, NoBuild, SetupPyStrategy}; +use crate::BuildIsolation; +use uv_configuration::{BuildKind, NoBinary, NoBuild, SetupPyStrategy}; /// Avoids cyclic crate dependencies between resolver, installer and builder. /// diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index a9f4d76be..bea8b34aa 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -32,6 +32,7 @@ uv-normalize = { workspace = true } uv-requirements = { workspace = true } uv-resolver = { workspace = true, features = ["clap"] } uv-types = { workspace = true, features = ["clap"] } +uv-configuration = { workspace = true, features = ["clap"] } uv-virtualenv = { workspace = true } uv-warnings = { workspace = true } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 55c781921..a635c6ed0 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -22,6 +22,10 @@ use uv_cache::Cache; use uv_client::{ BaseClientBuilder, Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder, }; +use uv_configuration::{ + ConfigSettings, Constraints, IndexStrategy, NoBinary, NoBuild, Overrides, SetupPyStrategy, + Upgrade, +}; use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_installer::Downloader; @@ -35,10 +39,7 @@ use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, Exclusions, InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, }; -use uv_types::{ - BuildIsolation, ConfigSettings, Constraints, EmptyInstalledPackages, InFlight, IndexStrategy, - NoBinary, NoBuild, Overrides, SetupPyStrategy, Upgrade, -}; +use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, ResolverReporter}; diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index af1bb8c32..bc002de4a 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -24,6 +24,10 @@ use uv_client::{ BaseClientBuilder, Connectivity, FlatIndex, FlatIndexClient, RegistryClient, RegistryClientBuilder, }; +use uv_configuration::{ + ConfigSettings, Constraints, IndexStrategy, NoBinary, NoBuild, Overrides, Reinstall, + SetupPyStrategy, Upgrade, +}; use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_installer::{BuiltEditable, Downloader, Plan, Planner, ResolvedEditable, SitePackages}; @@ -37,10 +41,7 @@ use uv_resolver::{ DependencyMode, Exclusions, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_types::{ - BuildIsolation, ConfigSettings, Constraints, InFlight, IndexStrategy, NoBinary, NoBuild, - Overrides, Reinstall, SetupPyStrategy, Upgrade, -}; +use uv_types::{BuildIsolation, InFlight}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 289281614..5229613b7 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -19,6 +19,9 @@ use uv_client::{ BaseClientBuilder, Connectivity, FlatIndex, FlatIndexClient, RegistryClient, RegistryClientBuilder, }; +use uv_configuration::{ + ConfigSettings, IndexStrategy, NoBinary, NoBuild, Reinstall, SetupPyStrategy, +}; use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_installer::{is_dynamic, Downloader, Plan, Planner, ResolvedEditable, SitePackages}; @@ -28,10 +31,7 @@ use uv_requirements::{ SourceTreeResolver, }; use uv_resolver::{DependencyMode, InMemoryIndex, Manifest, OptionsBuilder, Resolver}; -use uv_types::{ - BuildIsolation, ConfigSettings, EmptyInstalledPackages, InFlight, IndexStrategy, NoBinary, - NoBuild, Reinstall, SetupPyStrategy, -}; +use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 160e7e332..153a5f756 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -16,14 +16,12 @@ use pep508_rs::Requirement; use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE}; use uv_cache::Cache; use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder}; +use uv_configuration::{ConfigSettings, IndexStrategy, NoBinary, NoBuild, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_resolver::{InMemoryIndex, OptionsBuilder}; -use uv_types::{ - BuildContext, BuildIsolation, ConfigSettings, InFlight, IndexStrategy, NoBinary, NoBuild, - SetupPyStrategy, -}; +use uv_types::{BuildContext, BuildIsolation, InFlight}; use crate::commands::ExitStatus; use crate::printer::Printer; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 913069bf4..134e11a30 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -16,15 +16,15 @@ use distribution_types::{FlatIndexLocation, IndexLocations, IndexUrl}; use uv_auth::KeyringProvider; use uv_cache::{Cache, CacheArgs, Refresh}; use uv_client::Connectivity; +use uv_configuration::{ + ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, Reinstall, SetupPyStrategy, + Upgrade, +}; +use uv_configuration::{IndexStrategy, NoBinary}; use uv_interpreter::PythonVersion; use uv_normalize::{ExtraName, PackageName}; use uv_requirements::{ExtrasSpecification, RequirementsSource}; use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode}; -use uv_types::{ - ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, Reinstall, SetupPyStrategy, - Upgrade, -}; -use uv_types::{IndexStrategy, NoBinary}; use crate::commands::{extra_name_with_clap_error, ExitStatus, ListFormat, VersionFormat}; use crate::compat::CompatArgs; From 13ae5ac8dc1e49b513fd576ccdd39c9c890718c6 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 9 Apr 2024 12:56:16 -0400 Subject: [PATCH 069/110] Replace PyPI-internal Hashes representation with flat vector (#2925) ## Summary Right now, we have a `Hashes` representation that looks like: ```rust /// A dictionary mapping a hash name to a hex encoded digest of the file. /// /// PEP 691 says multiple hashes can be included and the interpretation is left to the client. #[derive(Debug, Clone, Eq, PartialEq, Default, Deserialize)] pub struct Hashes { pub md5: Option>, pub sha256: Option>, pub sha384: Option>, pub sha512: Option>, } ``` It stems from the PyPI API, which returns a dictionary of hashes. We tend to pass these around as a vector of `Vec`. But it's a bit strange because each entry in that vector could contain multiple hashes. And it makes it difficult to ask questions like "Is `sha256:ab21378ca980a8` in the set of hashes"? This PR instead treats `Hashes` as the PyPI-internal type, and uses a new `Vec` everywhere in our own APIs. --- crates/distribution-types/src/file.rs | 13 +- crates/distribution-types/src/lib.rs | 8 +- .../src/prioritized_distribution.rs | 30 +-- crates/pypi-types/src/simple_json.rs | 189 +++++++++++++----- crates/uv-cache/src/lib.rs | 2 +- crates/uv-client/src/flat_index.rs | 14 +- crates/uv-client/src/registry_client.rs | 6 +- crates/uv-resolver/src/preferences.rs | 12 +- crates/uv-resolver/src/resolution.rs | 14 +- crates/uv-resolver/src/version_map.rs | 10 +- 10 files changed, 195 insertions(+), 103 deletions(-) diff --git a/crates/distribution-types/src/file.rs b/crates/distribution-types/src/file.rs index 202202b44..ea31738ff 100644 --- a/crates/distribution-types/src/file.rs +++ b/crates/distribution-types/src/file.rs @@ -7,7 +7,7 @@ use url::Url; use pep440_rs::{VersionSpecifiers, VersionSpecifiersParseError}; use pep508_rs::split_scheme; -use pypi_types::{DistInfoMetadata, Hashes, Yanked}; +use pypi_types::{DistInfoMetadata, HashDigest, Yanked}; /// Error converting [`pypi_types::File`] to [`distribution_type::File`]. #[derive(Debug, Error)] @@ -25,9 +25,9 @@ pub enum FileConversionError { #[archive(check_bytes)] #[archive_attr(derive(Debug))] pub struct File { - pub dist_info_metadata: Option, + pub dist_info_metadata: bool, pub filename: String, - pub hashes: Hashes, + pub hashes: Vec, pub requires_python: Option, pub size: Option, // N.B. We don't use a chrono DateTime here because it's a little @@ -43,9 +43,12 @@ impl File { /// `TryFrom` instead of `From` to filter out files with invalid requires python version specifiers pub fn try_from(file: pypi_types::File, base: &Url) -> Result { Ok(Self { - dist_info_metadata: file.dist_info_metadata, + dist_info_metadata: file + .dist_info_metadata + .as_ref() + .is_some_and(DistInfoMetadata::is_available), filename: file.filename, - hashes: file.hashes, + hashes: file.hashes.into_digests(), requires_python: file .requires_python .transpose() diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index 95ca6c912..4b84e6e38 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -774,16 +774,16 @@ impl Identifier for Url { impl Identifier for File { fn distribution_id(&self) -> DistributionId { - if let Some(hash) = self.hashes.as_str() { - DistributionId::new(hash) + if let Some(hash) = self.hashes.first() { + DistributionId::new(&*hash.digest) } else { self.url.distribution_id() } } fn resource_id(&self) -> ResourceId { - if let Some(hash) = self.hashes.as_str() { - ResourceId::new(hash) + if let Some(hash) = self.hashes.first() { + ResourceId::new(&*hash.digest) } else { self.url.resource_id() } diff --git a/crates/distribution-types/src/prioritized_distribution.rs b/crates/distribution-types/src/prioritized_distribution.rs index 60f98adba..dc67a0f34 100644 --- a/crates/distribution-types/src/prioritized_distribution.rs +++ b/crates/distribution-types/src/prioritized_distribution.rs @@ -2,7 +2,7 @@ use std::fmt::{Display, Formatter}; use pep440_rs::VersionSpecifiers; use platform_tags::{IncompatibleTag, TagCompatibility, TagPriority}; -use pypi_types::{Hashes, Yanked}; +use pypi_types::{HashDigest, Yanked}; use crate::{Dist, InstalledDist, ResolvedDistRef}; @@ -18,7 +18,7 @@ struct PrioritizedDistInner { /// The highest-priority wheel. wheel: Option<(Dist, WheelCompatibility)>, /// The hashes for each distribution. - hashes: Vec, + hashes: Vec, } /// A distribution that can be used for both resolution and installation. @@ -141,24 +141,28 @@ pub enum IncompatibleSource { impl PrioritizedDist { /// Create a new [`PrioritizedDist`] from the given wheel distribution. - pub fn from_built(dist: Dist, hash: Option, compatibility: WheelCompatibility) -> Self { + pub fn from_built( + dist: Dist, + hashes: Vec, + compatibility: WheelCompatibility, + ) -> Self { Self(Box::new(PrioritizedDistInner { wheel: Some((dist, compatibility)), source: None, - hashes: hash.map(|hash| vec![hash]).unwrap_or_default(), + hashes, })) } /// Create a new [`PrioritizedDist`] from the given source distribution. pub fn from_source( dist: Dist, - hash: Option, + hashes: Vec, compatibility: SourceDistCompatibility, ) -> Self { Self(Box::new(PrioritizedDistInner { wheel: None, source: Some((dist, compatibility)), - hashes: hash.map(|hash| vec![hash]).unwrap_or_default(), + hashes, })) } @@ -166,7 +170,7 @@ impl PrioritizedDist { pub fn insert_built( &mut self, dist: Dist, - hash: Option, + hashes: Vec, compatibility: WheelCompatibility, ) { // Track the highest-priority wheel. @@ -178,16 +182,14 @@ impl PrioritizedDist { self.0.wheel = Some((dist, compatibility)); } - if let Some(hash) = hash { - self.0.hashes.push(hash); - } + self.0.hashes.extend(hashes); } /// Insert the given source distribution into the [`PrioritizedDist`]. pub fn insert_source( &mut self, dist: Dist, - hash: Option, + hashes: Vec, compatibility: SourceDistCompatibility, ) { // Track the highest-priority source. @@ -199,9 +201,7 @@ impl PrioritizedDist { self.0.source = Some((dist, compatibility)); } - if let Some(hash) = hash { - self.0.hashes.push(hash); - } + self.0.hashes.extend(hashes); } /// Return the highest-priority distribution for the package version, if any. @@ -274,7 +274,7 @@ impl PrioritizedDist { } /// Return the hashes for each distribution. - pub fn hashes(&self) -> &[Hashes] { + pub fn hashes(&self) -> &[HashDigest] { &self.0.hashes } diff --git a/crates/pypi-types/src/simple_json.rs b/crates/pypi-types/src/simple_json.rs index 342d54b2b..199b3c7c5 100644 --- a/crates/pypi-types/src/simple_json.rs +++ b/crates/pypi-types/src/simple_json.rs @@ -68,11 +68,7 @@ where )) } -#[derive( - Debug, Clone, Serialize, Deserialize, rkyv::Archive, rkyv::Deserialize, rkyv::Serialize, -)] -#[archive(check_bytes)] -#[archive_attr(derive(Debug))] +#[derive(Debug, Clone, Deserialize)] #[serde(untagged)] pub enum DistInfoMetadata { Bool(bool), @@ -125,23 +121,7 @@ impl Default for Yanked { /// A dictionary mapping a hash name to a hex encoded digest of the file. /// /// PEP 691 says multiple hashes can be included and the interpretation is left to the client. -#[derive( - Debug, - Clone, - Ord, - PartialOrd, - Eq, - PartialEq, - Hash, - Default, - Serialize, - Deserialize, - rkyv::Archive, - rkyv::Deserialize, - rkyv::Serialize, -)] -#[archive(check_bytes)] -#[archive_attr(derive(Debug))] +#[derive(Debug, Clone, Eq, PartialEq, Default, Deserialize)] pub struct Hashes { pub md5: Option>, pub sha256: Option>, @@ -150,31 +130,34 @@ pub struct Hashes { } impl Hashes { - /// Format as `:`. - pub fn to_string(&self) -> Option { - self.sha512 - .as_ref() - .map(|sha512| format!("sha512:{sha512}")) - .or_else(|| { - self.sha384 - .as_ref() - .map(|sha384| format!("sha384:{sha384}")) - }) - .or_else(|| { - self.sha256 - .as_ref() - .map(|sha256| format!("sha256:{sha256}")) - }) - .or_else(|| self.md5.as_ref().map(|md5| format!("md5:{md5}"))) - } - - /// Return the hash digest. - pub fn as_str(&self) -> Option<&str> { - self.sha512 - .as_deref() - .or(self.sha384.as_deref()) - .or(self.sha256.as_deref()) - .or(self.md5.as_deref()) + /// Convert a set of [`Hashes`] into a list of [`HashDigest`]s. + pub fn into_digests(self) -> Vec { + let mut digests = Vec::new(); + if let Some(sha512) = self.sha512 { + digests.push(HashDigest { + algorithm: HashAlgorithm::Sha512, + digest: sha512, + }); + } + if let Some(sha384) = self.sha384 { + digests.push(HashDigest { + algorithm: HashAlgorithm::Sha384, + digest: sha384, + }); + } + if let Some(sha256) = self.sha256 { + digests.push(HashDigest { + algorithm: HashAlgorithm::Sha256, + digest: sha256, + }); + } + if let Some(md5) = self.md5 { + digests.push(HashDigest { + algorithm: HashAlgorithm::Md5, + digest: md5, + }); + } + digests } } @@ -239,6 +222,118 @@ impl FromStr for Hashes { } } +#[derive( + Debug, + Clone, + Copy, + Ord, + PartialOrd, + Eq, + PartialEq, + Hash, + Serialize, + Deserialize, + rkyv::Archive, + rkyv::Deserialize, + rkyv::Serialize, +)] +#[archive(check_bytes)] +#[archive_attr(derive(Debug))] +pub enum HashAlgorithm { + Md5, + Sha256, + Sha384, + Sha512, +} + +impl FromStr for HashAlgorithm { + type Err = HashError; + + fn from_str(s: &str) -> Result { + match s { + "md5" => Ok(Self::Md5), + "sha256" => Ok(Self::Sha256), + "sha384" => Ok(Self::Sha384), + "sha512" => Ok(Self::Sha512), + _ => Err(HashError::UnsupportedHashAlgorithm(s.to_string())), + } + } +} + +impl std::fmt::Display for HashAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Md5 => write!(f, "md5"), + Self::Sha256 => write!(f, "sha256"), + Self::Sha384 => write!(f, "sha384"), + Self::Sha512 => write!(f, "sha512"), + } + } +} + +/// A hash name and hex encoded digest of the file. +#[derive( + Debug, + Clone, + Ord, + PartialOrd, + Eq, + PartialEq, + Hash, + Serialize, + Deserialize, + rkyv::Archive, + rkyv::Deserialize, + rkyv::Serialize, +)] +#[archive(check_bytes)] +#[archive_attr(derive(Debug))] +pub struct HashDigest { + pub algorithm: HashAlgorithm, + pub digest: Box, +} + +impl HashDigest { + /// Return the [`HashAlgorithm`] of the digest. + pub fn algorithm(&self) -> HashAlgorithm { + self.algorithm + } +} + +impl std::fmt::Display for HashDigest { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}:{}", self.algorithm, self.digest) + } +} + +impl FromStr for HashDigest { + type Err = HashError; + + fn from_str(s: &str) -> Result { + let mut parts = s.split(':'); + + // Extract the key and value. + let name = parts + .next() + .ok_or_else(|| HashError::InvalidStructure(s.to_string()))?; + let value = parts + .next() + .ok_or_else(|| HashError::InvalidStructure(s.to_string()))?; + + // Ensure there are no more parts. + if parts.next().is_some() { + return Err(HashError::InvalidStructure(s.to_string())); + } + + let algorithm = HashAlgorithm::from_str(name)?; + + Ok(HashDigest { + algorithm, + digest: value.to_owned().into_boxed_str(), + }) + } +} + #[derive(thiserror::Error, Debug)] pub enum HashError { #[error("Unexpected hash (expected `:`): {0}")] diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index 05082ac68..d2e5b3c41 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -598,7 +598,7 @@ impl CacheBucket { Self::FlatIndex => "flat-index-v0", Self::Git => "git-v0", Self::Interpreter => "interpreter-v0", - Self::Simple => "simple-v6", + Self::Simple => "simple-v7", Self::Wheels => "wheels-v0", Self::Archive => "archive-v0", } diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index d44b28464..5577af957 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -17,7 +17,7 @@ use distribution_types::{ use pep440_rs::Version; use pep508_rs::VerbatimUrl; use platform_tags::Tags; -use pypi_types::Hashes; + use uv_cache::{Cache, CacheBucket}; use uv_configuration::{NoBinary, NoBuild}; use uv_normalize::PackageName; @@ -236,9 +236,9 @@ impl<'a> FlatIndexClient<'a> { }; let file = File { - dist_info_metadata: None, + dist_info_metadata: false, filename: filename.to_string(), - hashes: Hashes::default(), + hashes: Vec::new(), requires_python: None, size: None, upload_time_utc_ms: None, @@ -323,10 +323,10 @@ impl FlatIndex { })); match distributions.0.entry(version) { Entry::Occupied(mut entry) => { - entry.get_mut().insert_built(dist, None, compatibility); + entry.get_mut().insert_built(dist, vec![], compatibility); } Entry::Vacant(entry) => { - entry.insert(PrioritizedDist::from_built(dist, None, compatibility)); + entry.insert(PrioritizedDist::from_built(dist, vec![], compatibility)); } } } @@ -339,10 +339,10 @@ impl FlatIndex { })); match distributions.0.entry(filename.version) { Entry::Occupied(mut entry) => { - entry.get_mut().insert_source(dist, None, compatibility); + entry.get_mut().insert_source(dist, vec![], compatibility); } Entry::Vacant(entry) => { - entry.insert(PrioritizedDist::from_source(dist, None, compatibility)); + entry.insert(PrioritizedDist::from_source(dist, vec![], compatibility)); } } } diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 2b740e745..6fd5b5020 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -424,11 +424,7 @@ impl RegistryClient { ) -> Result { // If the metadata file is available at its own url (PEP 658), download it from there. let filename = WheelFilename::from_str(&file.filename).map_err(ErrorKind::WheelFilename)?; - if file - .dist_info_metadata - .as_ref() - .is_some_and(pypi_types::DistInfoMetadata::is_available) - { + if file.dist_info_metadata { let mut url = url.clone(); url.set_path(&format!("{}.metadata", url.path())); diff --git a/crates/uv-resolver/src/preferences.rs b/crates/uv-resolver/src/preferences.rs index 95bccb464..376ceaf67 100644 --- a/crates/uv-resolver/src/preferences.rs +++ b/crates/uv-resolver/src/preferences.rs @@ -6,7 +6,7 @@ use pep440_rs::{Operator, Version}; use pep508_rs::{ MarkerEnvironment, Requirement, RequirementsTxtRequirement, UnnamedRequirement, VersionOrUrl, }; -use pypi_types::{HashError, Hashes}; +use pypi_types::{HashDigest, HashError}; use requirements_txt::RequirementEntry; use tracing::trace; use uv_normalize::PackageName; @@ -23,7 +23,7 @@ pub enum PreferenceError { #[derive(Clone, Debug)] pub struct Preference { requirement: Requirement, - hashes: Vec, + hashes: Vec, } impl Preference { @@ -40,7 +40,7 @@ impl Preference { .hashes .iter() .map(String::as_str) - .map(Hashes::from_str) + .map(HashDigest::from_str) .collect::>()?, }) } @@ -146,7 +146,7 @@ impl Preferences { &self, package_name: &PackageName, version: &Version, - ) -> Option<&[Hashes]> { + ) -> Option<&[HashDigest]> { self.0 .get(package_name) .filter(|pin| pin.version() == version) @@ -158,7 +158,7 @@ impl Preferences { #[derive(Debug, Clone)] struct Pin { version: Version, - hashes: Vec, + hashes: Vec, } impl Pin { @@ -168,7 +168,7 @@ impl Pin { } /// Return the hashes of the pinned package. - fn hashes(&self) -> &[Hashes] { + fn hashes(&self) -> &[HashDigest] { &self.hashes } } diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index c3e0dcb84..da93cce16 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -18,7 +18,7 @@ use distribution_types::{ use once_map::OnceMap; use pep440_rs::Version; use pep508_rs::MarkerEnvironment; -use pypi_types::Hashes; +use pypi_types::HashDigest; use uv_distribution::to_precise; use uv_normalize::{ExtraName, PackageName}; @@ -50,7 +50,7 @@ pub struct ResolutionGraph { /// The underlying graph. petgraph: petgraph::graph::Graph, petgraph::Directed>, /// The metadata for every distribution in this resolution. - hashes: FxHashMap>, + hashes: FxHashMap>, /// The enabled extras for every distribution in this resolution. extras: FxHashMap>, /// The set of editable requirements in this resolution. @@ -649,12 +649,10 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> { .filter(|hashes| !hashes.is_empty()) { for hash in hashes { - if let Some(hash) = hash.to_string() { - has_hashes = true; - line.push_str(" \\\n"); - line.push_str(" --hash="); - line.push_str(&hash); - } + has_hashes = true; + line.push_str(" \\\n"); + line.push_str(" --hash="); + line.push_str(&hash.to_string()); } } } diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 4eb5f5285..8934f98f0 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -12,7 +12,7 @@ use distribution_types::{ }; use pep440_rs::{Version, VersionSpecifiers}; use platform_tags::Tags; -use pypi_types::{Hashes, Yanked}; +use pypi_types::{HashDigest, Yanked}; use rkyv::{de::deserializers::SharedDeserializeMap, Deserialize}; use uv_client::{FlatDistributions, OwnedArchive, SimpleMetadata, VersionFiles}; use uv_configuration::{NoBinary, NoBuild}; @@ -176,7 +176,7 @@ impl VersionMap { } /// Return the [`Hashes`] for the given version, if any. - pub(crate) fn hashes(&self, version: &Version) -> Option> { + pub(crate) fn hashes(&self, version: &Version) -> Option> { match self.inner { VersionMapInner::Eager(ref map) => map.get(version).map(|file| file.hashes().to_vec()), VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(|file| file.hashes().to_vec()), @@ -378,7 +378,7 @@ impl VersionMapLazy { let version = filename.version().clone(); let requires_python = file.requires_python.clone(); let yanked = file.yanked.clone(); - let hash = file.hashes.clone(); + let hashes = file.hashes.clone(); match filename { DistFilename::WheelFilename(filename) => { let compatibility = self.wheel_compatibility( @@ -394,7 +394,7 @@ impl VersionMapLazy { file, self.index.clone(), ); - priority_dist.insert_built(dist, Some(hash), compatibility); + priority_dist.insert_built(dist, hashes, compatibility); } DistFilename::SourceDistFilename(filename) => { let compatibility = self.source_dist_compatibility( @@ -409,7 +409,7 @@ impl VersionMapLazy { file, self.index.clone(), ); - priority_dist.insert_source(dist, Some(hash), compatibility); + priority_dist.insert_source(dist, hashes, compatibility); } } } From 83e2297633476a826f964c9f2b33c26986301f87 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 9 Apr 2024 13:30:02 -0400 Subject: [PATCH 070/110] Store common fields on `BuiltWheelIndex` struct (#2939) ## Summary This mirrors the structure of the `RegistryWheelIndex`. It will be useful once these indexes check hashes too. --- .../src/index/built_wheel_index.rs | 45 ++++++++++--------- .../src/index/registry_wheel_index.rs | 2 +- crates/uv-installer/src/plan.rs | 7 +-- 3 files changed, 28 insertions(+), 26 deletions(-) diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index df60c51ee..ca4f59b45 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -8,20 +8,25 @@ use crate::source::{read_http_revision, read_timestamped_revision, REVISION}; use crate::Error; /// A local index of built distributions for a specific source distribution. -pub struct BuiltWheelIndex; +#[derive(Debug)] +pub struct BuiltWheelIndex<'a> { + cache: &'a Cache, + tags: &'a Tags, +} + +impl<'a> BuiltWheelIndex<'a> { + /// Initialize an index of built distributions. + pub fn new(cache: &'a Cache, tags: &'a Tags) -> Self { + Self { cache, tags } + } -impl BuiltWheelIndex { /// Return the most compatible [`CachedWheel`] for a given source distribution at a direct URL. /// /// This method does not perform any freshness checks and assumes that the source distribution /// is already up-to-date. - pub fn url( - source_dist: &DirectUrlSourceDist, - cache: &Cache, - tags: &Tags, - ) -> Result, Error> { + pub fn url(&self, source_dist: &DirectUrlSourceDist) -> Result, Error> { // For direct URLs, cache directly under the hash of the URL itself. - let cache_shard = cache.shard( + let cache_shard = self.cache.shard( CacheBucket::BuiltWheels, WheelCache::Url(source_dist.url.raw()).root(), ); @@ -33,16 +38,12 @@ impl BuiltWheelIndex { return Ok(None); }; - Ok(Self::find(&cache_shard.shard(revision.id()), tags)) + Ok(self.find(&cache_shard.shard(revision.id()))) } /// Return the most compatible [`CachedWheel`] for a given source distribution at a local path. - pub fn path( - source_dist: &PathSourceDist, - cache: &Cache, - tags: &Tags, - ) -> Result, Error> { - let cache_shard = cache.shard( + pub fn path(&self, source_dist: &PathSourceDist) -> Result, Error> { + let cache_shard = self.cache.shard( CacheBucket::BuiltWheels, WheelCache::Path(&source_dist.url).root(), ); @@ -61,21 +62,21 @@ impl BuiltWheelIndex { return Ok(None); }; - Ok(Self::find(&cache_shard.shard(revision.id()), tags)) + Ok(self.find(&cache_shard.shard(revision.id()))) } /// Return the most compatible [`CachedWheel`] for a given source distribution at a git URL. - pub fn git(source_dist: &GitSourceDist, cache: &Cache, tags: &Tags) -> Option { + pub fn git(&self, source_dist: &GitSourceDist) -> Option { let Ok(Some(git_sha)) = git_reference(&source_dist.url) else { return None; }; - let cache_shard = cache.shard( + let cache_shard = self.cache.shard( CacheBucket::BuiltWheels, WheelCache::Git(&source_dist.url, &git_sha.to_short_string()).root(), ); - Self::find(&cache_shard, tags) + self.find(&cache_shard) } /// Find the "best" distribution in the index for a given source distribution. @@ -94,7 +95,7 @@ impl BuiltWheelIndex { /// ``` /// /// The `shard` should be `built-wheels-v0/pypi/django-allauth-0.51.0.tar.gz`. - fn find(shard: &CacheShard, tags: &Tags) -> Option { + fn find(&self, shard: &CacheShard) -> Option { let mut candidate: Option = None; // Unzipped wheels are stored as symlinks into the archive directory. @@ -103,7 +104,7 @@ impl BuiltWheelIndex { None => {} Some(dist_info) => { // Pick the wheel with the highest priority - let compatibility = dist_info.filename.compatibility(tags); + let compatibility = dist_info.filename.compatibility(self.tags); // Only consider wheels that are compatible with our tags. if !compatibility.is_compatible() { @@ -113,7 +114,7 @@ impl BuiltWheelIndex { if let Some(existing) = candidate.as_ref() { // Override if the wheel is newer, or "more" compatible. if dist_info.filename.version > existing.filename.version - || compatibility > existing.filename.compatibility(tags) + || compatibility > existing.filename.compatibility(self.tags) { candidate = Some(dist_info); } diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index fc5fceb18..d1dbf251f 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -25,7 +25,7 @@ pub struct RegistryWheelIndex<'a> { } impl<'a> RegistryWheelIndex<'a> { - /// Initialize an index of cached distributions from a directory. + /// Initialize an index of registry distributions. pub fn new(cache: &'a Cache, tags: &'a Tags, index_locations: &'a IndexLocations) -> Self { Self { cache, diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 985a6f732..25f74a48d 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -66,6 +66,7 @@ impl<'a> Planner<'a> { ) -> Result { // Index all the already-downloaded wheels in the cache. let mut registry_index = RegistryWheelIndex::new(cache, tags, index_locations); + let built_index = BuiltWheelIndex::new(cache, tags); let mut cached = vec![]; let mut remote = vec![]; @@ -323,7 +324,7 @@ impl<'a> Planner<'a> { Dist::Source(SourceDist::DirectUrl(sdist)) => { // Find the most-compatible wheel from the cache, since we don't know // the filename in advance. - if let Some(wheel) = BuiltWheelIndex::url(&sdist, cache, tags)? { + if let Some(wheel) = built_index.url(&sdist)? { let cached_dist = wheel.into_url_dist(url.clone()); debug!("URL source requirement already cached: {cached_dist}"); cached.push(CachedDist::Url(cached_dist)); @@ -333,7 +334,7 @@ impl<'a> Planner<'a> { Dist::Source(SourceDist::Path(sdist)) => { // Find the most-compatible wheel from the cache, since we don't know // the filename in advance. - if let Some(wheel) = BuiltWheelIndex::path(&sdist, cache, tags)? { + if let Some(wheel) = built_index.path(&sdist)? { let cached_dist = wheel.into_url_dist(url.clone()); debug!("Path source requirement already cached: {cached_dist}"); cached.push(CachedDist::Url(cached_dist)); @@ -343,7 +344,7 @@ impl<'a> Planner<'a> { Dist::Source(SourceDist::Git(sdist)) => { // Find the most-compatible wheel from the cache, since we don't know // the filename in advance. - if let Some(wheel) = BuiltWheelIndex::git(&sdist, cache, tags) { + if let Some(wheel) = built_index.git(&sdist) { let cached_dist = wheel.into_url_dist(url.clone()); debug!("Git source requirement already cached: {cached_dist}"); cached.push(CachedDist::Url(cached_dist)); From f9c06329538577923c7514da6dca60864f22e407 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 9 Apr 2024 15:09:41 -0400 Subject: [PATCH 071/110] Ignore direct URL distributions in prefetcher (#2943) ## Summary The prefetcher tallies the number of times we tried a given package, and then once we hit a threshold, grabs the version map, assuming it's already been fetched. For direct URL distributions, though, we don't have a version map! And there's no need to prefetch. Closes https://github.com/astral-sh/uv/issues/2941. --- crates/uv-resolver/src/resolver/batch_prefetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/uv-resolver/src/resolver/batch_prefetch.rs b/crates/uv-resolver/src/resolver/batch_prefetch.rs index 42f8e6754..93d32c4fa 100644 --- a/crates/uv-resolver/src/resolver/batch_prefetch.rs +++ b/crates/uv-resolver/src/resolver/batch_prefetch.rs @@ -52,7 +52,7 @@ impl BatchPrefetcher { index: &InMemoryIndex, selector: &CandidateSelector, ) -> anyhow::Result<(), ResolveError> { - let PubGrubPackage::Package(package_name, _, _) = &next else { + let PubGrubPackage::Package(package_name, None, None) = &next else { return Ok(()); }; From 7bcca28b12cd12a6fdf902ec8ae2c48630ba7356 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 9 Apr 2024 15:20:43 -0400 Subject: [PATCH 072/110] Bump version to v0.1.31 (#2944) --- CHANGELOG.md | 6 ++++++ Cargo.lock | 4 ++-- crates/uv-version/Cargo.toml | 2 +- crates/uv/Cargo.toml | 2 +- pyproject.toml | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 634a217eb..eba387b78 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## 0.1.31 + +### Bug fixes + +- Ignore direct URL distributions in prefetcher ([#2943](https://github.com/astral-sh/uv/pull/2943)) + ## 0.1.30 ### Enhancements diff --git a/Cargo.lock b/Cargo.lock index 244e925c6..2912918d4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4280,7 +4280,7 @@ checksum = "a183cf7feeba97b4dd1c0d46788634f6221d87fa961b305bed08c851829efcc0" [[package]] name = "uv" -version = "0.1.30" +version = "0.1.31" dependencies = [ "anstream", "anyhow", @@ -4822,7 +4822,7 @@ dependencies = [ [[package]] name = "uv-version" -version = "0.1.30" +version = "0.1.31" [[package]] name = "uv-virtualenv" diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml index 6f35ba3aa..47b3a0440 100644 --- a/crates/uv-version/Cargo.toml +++ b/crates/uv-version/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv-version" -version = "0.1.30" +version = "0.1.31" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index bea8b34aa..83588c1b0 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "uv" -version = "0.1.30" +version = "0.1.31" edition = { workspace = true } rust-version = { workspace = true } homepage = { workspace = true } diff --git a/pyproject.toml b/pyproject.toml index 7ef559e57..b4126bac9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "maturin" [project] name = "uv" -version = "0.1.30" +version = "0.1.31" description = "An extremely fast Python package installer and resolver, written in Rust." authors = [{ name = "Astral Software Inc.", email = "hey@astral.sh" }] requires-python = ">=3.8" From ee9059978a1e998cac4da805f01d99c760edaad6 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Tue, 9 Apr 2024 21:29:39 -0500 Subject: [PATCH 073/110] Add ecosystem test for Prefect (#2942) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reproduced https://github.com/astral-sh/uv/issues/2941 and confirmed fix. We probably ought to have some ecosystem test coverage — this seems like a good starting point we can extend to other projects in the future. --- .github/workflows/ci.yml | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b5aabf16a..a84265926 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -224,6 +224,36 @@ jobs: path: ./target/debug/uv.exe retention-days: 1 + ecosystem-test-prefect: + needs: build-binary-linux + name: "ecosystem test | prefect" + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - uses: actions/setup-python@v5 + with: + python-version: "3.9" + + - name: "Download binary" + uses: actions/download-artifact@v4 + with: + name: uv-linux-${{ github.sha }} + + - name: "Clone project" + run: gh repo clone prefecthq/prefect + env: + GH_TOKEN: ${{ github.token }} + + - name: "Prepare binary" + run: chmod +x ./uv + + - name: "Test" + run: | + cd prefect + ../uv venv + ../uv pip install --upgrade -e '.[dev]' + cache-test-ubuntu: needs: build-binary-linux name: "check cache | ubuntu" From 7ae06b3b46b96be579713a392d8161ac639bc9ab Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 9 Apr 2024 23:12:10 -0400 Subject: [PATCH 074/110] Surface invalid metadata as hints in error reports (#2850) ## Summary Closes #2847. --- Cargo.lock | 1 + crates/uv-resolver/Cargo.toml | 1 + crates/uv-resolver/src/error.rs | 31 +++++- crates/uv-resolver/src/pubgrub/report.rs | 136 ++++++++++++++++++++++- crates/uv-resolver/src/resolver/mod.rs | 75 +++++++++---- crates/uv/tests/pip_compile.rs | 9 ++ crates/uv/tests/pip_sync.rs | 3 + 7 files changed, 229 insertions(+), 27 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2912918d4..22ef029e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4786,6 +4786,7 @@ dependencies = [ "requirements-txt", "rkyv", "rustc-hash", + "textwrap", "thiserror", "tokio", "tokio-stream", diff --git a/crates/uv-resolver/Cargo.toml b/crates/uv-resolver/Cargo.toml index 1c8c5dbb3..55269d8c6 100644 --- a/crates/uv-resolver/Cargo.toml +++ b/crates/uv-resolver/Cargo.toml @@ -48,6 +48,7 @@ petgraph = { workspace = true } pubgrub = { workspace = true } rkyv = { workspace = true } rustc-hash = { workspace = true } +textwrap = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["macros"] } tokio-stream = { workspace = true } diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 96c46fcb1..623136dff 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -1,4 +1,4 @@ -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::fmt::Formatter; use std::ops::Deref; @@ -20,7 +20,7 @@ use crate::candidate_selector::CandidateSelector; use crate::dependency_provider::UvDependencyProvider; use crate::pubgrub::{PubGrubPackage, PubGrubPython, PubGrubReportFormatter}; use crate::python_requirement::PythonRequirement; -use crate::resolver::{UnavailablePackage, VersionsResponse}; +use crate::resolver::{IncompletePackage, UnavailablePackage, VersionsResponse}; #[derive(Debug, thiserror::Error)] pub enum ResolveError { @@ -125,6 +125,7 @@ impl From> for ResolveError { python_requirement: None, index_locations: None, unavailable_packages: FxHashMap::default(), + incomplete_packages: FxHashMap::default(), }) } pubgrub::error::PubGrubError::SelfDependency { package, version } => { @@ -146,6 +147,7 @@ pub struct NoSolutionError { python_requirement: Option, index_locations: Option, unavailable_packages: FxHashMap, + incomplete_packages: FxHashMap>, } impl std::error::Error for NoSolutionError {} @@ -167,6 +169,7 @@ impl std::fmt::Display for NoSolutionError { &self.selector, &self.index_locations, &self.unavailable_packages, + &self.incomplete_packages, ) { write!(f, "\n\n{hint}")?; } @@ -261,6 +264,30 @@ impl NoSolutionError { self } + /// Update the incomplete packages attached to the error. + #[must_use] + pub(crate) fn with_incomplete_packages( + mut self, + incomplete_packages: &DashMap>, + ) -> Self { + let mut new = FxHashMap::default(); + for package in self.derivation_tree.packages() { + if let PubGrubPackage::Package(name, ..) = package { + if let Some(entry) = incomplete_packages.get(name) { + let versions = entry.value(); + for entry in versions { + let (version, reason) = entry.pair(); + new.entry(name.clone()) + .or_insert_with(BTreeMap::default) + .insert(version.clone(), reason.clone()); + } + } + } + } + self.incomplete_packages = new; + self + } + /// Update the Python requirements attached to the error. #[must_use] pub(crate) fn with_python_requirement( diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index dfb93997a..da63129ef 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -1,6 +1,6 @@ use std::borrow::Cow; use std::cmp::Ordering; -use std::collections::BTreeSet; +use std::collections::{BTreeMap, BTreeSet}; use std::ops::Bound; use derivative::Derivative; @@ -17,7 +17,7 @@ use uv_normalize::PackageName; use crate::candidate_selector::CandidateSelector; use crate::python_requirement::PythonRequirement; -use crate::resolver::UnavailablePackage; +use crate::resolver::{IncompletePackage, UnavailablePackage}; use super::PubGrubPackage; @@ -342,6 +342,7 @@ impl PubGrubReportFormatter<'_> { selector: &Option, index_locations: &Option, unavailable_packages: &FxHashMap, + incomplete_packages: &FxHashMap>, ) -> IndexSet { /// Returns `true` if pre-releases were allowed for a package. fn allowed_prerelease(package: &PubGrubPackage, selector: &CandidateSelector) -> bool { @@ -354,7 +355,7 @@ impl PubGrubReportFormatter<'_> { let mut hints = IndexSet::default(); match derivation_tree { DerivationTree::External(external) => match external { - External::NoVersions(package, set, _) => { + External::Unavailable(package, set, _) | External::NoVersions(package, set, _) => { // Check for no versions due to pre-release options if let Some(selector) = selector { let any_prerelease = set.iter().any(|(start, end)| { @@ -404,6 +405,7 @@ impl PubGrubReportFormatter<'_> { index_locations.flat_index().peekable().peek().is_none(); if let PubGrubPackage::Package(name, ..) = package { + // Add hints due to the package being entirely unavailable. match unavailable_packages.get(name) { Some(UnavailablePackage::NoIndex) => { if no_find_links { @@ -413,13 +415,55 @@ impl PubGrubReportFormatter<'_> { Some(UnavailablePackage::Offline) => { hints.insert(PubGrubHint::Offline); } - _ => {} + Some(UnavailablePackage::InvalidMetadata(reason)) => { + hints.insert(PubGrubHint::InvalidPackageMetadata { + package: package.clone(), + reason: reason.clone(), + }); + } + Some(UnavailablePackage::InvalidStructure(reason)) => { + hints.insert(PubGrubHint::InvalidPackageStructure { + package: package.clone(), + reason: reason.clone(), + }); + } + Some(UnavailablePackage::NotFound) => {} + None => {} + } + + // Add hints due to the package being unavailable at specific versions. + if let Some(versions) = incomplete_packages.get(name) { + for (version, incomplete) in versions.iter().rev() { + if set.contains(version) { + match incomplete { + IncompletePackage::Offline => { + hints.insert(PubGrubHint::Offline); + } + IncompletePackage::InvalidMetadata(reason) => { + hints.insert(PubGrubHint::InvalidVersionMetadata { + package: package.clone(), + version: version.clone(), + reason: reason.clone(), + }); + } + IncompletePackage::InvalidStructure(reason) => { + hints.insert( + PubGrubHint::InvalidVersionStructure { + package: package.clone(), + version: version.clone(), + reason: reason.clone(), + }, + ); + } + } + break; + } + } } } } } External::NotRoot(..) => {} - External::Unavailable(..) => {} External::FromDependencyOf(..) => {} }, DerivationTree::Derived(derived) => { @@ -428,12 +472,14 @@ impl PubGrubReportFormatter<'_> { selector, index_locations, unavailable_packages, + incomplete_packages, )); hints.extend(self.hints( &derived.cause2, selector, index_locations, unavailable_packages, + incomplete_packages, )); } } @@ -462,8 +508,36 @@ pub(crate) enum PubGrubHint { /// Requirements were unavailable due to lookups in the index being disabled and no extra /// index was provided via `--find-links` NoIndex, - /// A package was not found in the registry, but + /// A package was not found in the registry, but network access was disabled. Offline, + /// Metadata for a package could not be parsed. + InvalidPackageMetadata { + package: PubGrubPackage, + #[derivative(PartialEq = "ignore", Hash = "ignore")] + reason: String, + }, + /// The structure of a package was invalid (e.g., multiple `.dist-info` directories). + InvalidPackageStructure { + package: PubGrubPackage, + #[derivative(PartialEq = "ignore", Hash = "ignore")] + reason: String, + }, + /// Metadata for a package version could not be parsed. + InvalidVersionMetadata { + package: PubGrubPackage, + #[derivative(PartialEq = "ignore", Hash = "ignore")] + version: Version, + #[derivative(PartialEq = "ignore", Hash = "ignore")] + reason: String, + }, + /// The structure of a package version was invalid (e.g., multiple `.dist-info` directories). + InvalidVersionStructure { + package: PubGrubPackage, + #[derivative(PartialEq = "ignore", Hash = "ignore")] + version: Version, + #[derivative(PartialEq = "ignore", Hash = "ignore")] + reason: String, + }, } impl std::fmt::Display for PubGrubHint { @@ -505,6 +579,56 @@ impl std::fmt::Display for PubGrubHint { ":".bold(), ) } + Self::InvalidPackageMetadata { package, reason } => { + write!( + f, + "{}{} Metadata for {} could not be parsed:\n{}", + "hint".bold().cyan(), + ":".bold(), + package.bold(), + textwrap::indent(reason, " ") + ) + } + Self::InvalidPackageStructure { package, reason } => { + write!( + f, + "{}{} The structure of {} was invalid:\n{}", + "hint".bold().cyan(), + ":".bold(), + package.bold(), + textwrap::indent(reason, " ") + ) + } + Self::InvalidVersionMetadata { + package, + version, + reason, + } => { + write!( + f, + "{}{} Metadata for {}=={} could not be parsed:\n{}", + "hint".bold().cyan(), + ":".bold(), + package.bold(), + version.bold(), + textwrap::indent(reason, " ") + ) + } + Self::InvalidVersionStructure { + package, + version, + reason, + } => { + write!( + f, + "{}{} The structure of {}=={} was invalid:\n{}", + "hint".bold().cyan(), + ":".bold(), + package.bold(), + version.bold(), + textwrap::indent(reason, " ") + ) + } } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 11ec5a9b0..2f2795c03 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -71,19 +71,30 @@ pub(crate) enum UnavailableVersion { IncompatibleDist(IncompatibleDist), } -/// The package is unavailable and cannot be used +/// The package is unavailable and cannot be used. #[derive(Debug, Clone)] pub(crate) 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`) + /// 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. Offline, - /// The package was not found in the registry + /// The package was not found in the registry. NotFound, + /// The package metadata was found, but could not be parsed. + InvalidMetadata(String), + /// The package has an invalid structure. + InvalidStructure(String), +} + +/// The package is unavailable at specific versions. +#[derive(Debug, Clone)] +pub(crate) enum IncompletePackage { + /// Network requests were disabled (i.e., `--offline`), and the wheel metadata was not found in the cache. + Offline, /// The wheel metadata was found, but could not be parsed. - InvalidMetadata, + InvalidMetadata(String), /// The wheel has an invalid structure. - InvalidStructure, + InvalidStructure(String), } enum ResolverVersion { @@ -113,8 +124,10 @@ pub struct Resolver< selector: CandidateSelector, index: &'a InMemoryIndex, installed_packages: &'a InstalledPackages, - /// Incompatibilities for packages that are entirely unavailable + /// Incompatibilities for packages that are entirely unavailable. unavailable_packages: DashMap, + /// Incompatibilities for packages that are unavailable at specific versions. + incomplete_packages: DashMap>, /// The set of all registry-based packages visited during resolution. visited: DashSet, reporter: Option>, @@ -185,6 +198,7 @@ impl< Ok(Self { index, unavailable_packages: DashMap::default(), + incomplete_packages: DashMap::default(), visited: DashSet::default(), selector: CandidateSelector::for_resolution(options, &manifest, markers), dependency_mode: options.dependency_mode, @@ -247,7 +261,8 @@ impl< .with_selector(self.selector.clone()) .with_python_requirement(&self.python_requirement) .with_index_locations(self.provider.index_locations()) - .with_unavailable_packages(&self.unavailable_packages), + .with_unavailable_packages(&self.unavailable_packages) + .with_incomplete_packages(&self.incomplete_packages), ) } else { err @@ -352,10 +367,10 @@ impl< UnavailablePackage::NotFound => { "was not found in the package registry" } - UnavailablePackage::InvalidMetadata => { + UnavailablePackage::InvalidMetadata(_) => { "was found, but the metadata could not be parsed" } - UnavailablePackage::InvalidStructure => { + UnavailablePackage::InvalidStructure(_) => { "was found, but has an invalid format" } }) @@ -624,14 +639,18 @@ impl< .insert(package_name.clone(), UnavailablePackage::Offline); return Ok(None); } - MetadataResponse::InvalidMetadata(_) => { - self.unavailable_packages - .insert(package_name.clone(), UnavailablePackage::InvalidMetadata); + MetadataResponse::InvalidMetadata(err) => { + self.unavailable_packages.insert( + package_name.clone(), + UnavailablePackage::InvalidMetadata(err.to_string()), + ); return Ok(None); } - MetadataResponse::InvalidStructure(_) => { - self.unavailable_packages - .insert(package_name.clone(), UnavailablePackage::InvalidStructure); + MetadataResponse::InvalidStructure(err) => { + self.unavailable_packages.insert( + package_name.clone(), + UnavailablePackage::InvalidStructure(err.to_string()), + ); return Ok(None); } }; @@ -913,20 +932,38 @@ impl< .await .ok_or(ResolveError::Unregistered)?; - let metadata = match *response { - MetadataResponse::Found(ref metadata) => metadata, + let metadata = match &*response { + MetadataResponse::Found(metadata) => metadata, MetadataResponse::Offline => { + self.incomplete_packages + .entry(package_name.clone()) + .or_default() + .insert(version.clone(), IncompletePackage::Offline); return Ok(Dependencies::Unavailable( "network connectivity is disabled, but the metadata wasn't found in the cache" .to_string(), )); } - MetadataResponse::InvalidMetadata(_) => { + MetadataResponse::InvalidMetadata(err) => { + self.incomplete_packages + .entry(package_name.clone()) + .or_default() + .insert( + version.clone(), + IncompletePackage::InvalidMetadata(err.to_string()), + ); return Ok(Dependencies::Unavailable( "the package metadata could not be parsed".to_string(), )); } - MetadataResponse::InvalidStructure(_) => { + MetadataResponse::InvalidStructure(err) => { + self.incomplete_packages + .entry(package_name.clone()) + .or_default() + .insert( + version.clone(), + IncompletePackage::InvalidStructure(err.to_string()), + ); return Ok(Dependencies::Unavailable( "the package has an invalid format".to_string(), )); diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 292d4507b..4bff90244 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -4555,6 +4555,12 @@ fn invalid_metadata_requires_python() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because validation==2.0.0 is unusable because the package metadata could not be parsed and you require validation==2.0.0, we can conclude that the requirements are unsatisfiable. + + hint: Metadata for validation==2.0.0 could not be parsed: + Failed to parse version: Unexpected end of version specifier, expected operator: + 12 + ^^ + "### ); @@ -4581,6 +4587,9 @@ fn invalid_metadata_multiple_dist_info() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because validation==3.0.0 is unusable because the package has an invalid format and you require validation==3.0.0, we can conclude that the requirements are unsatisfiable. + + hint: The structure of validation==3.0.0 was invalid: + Multiple .dist-info directories found: validation-2.0.0, validation-3.0.0 "### ); diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index b387eb9ac..202d3a9c6 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -1106,6 +1106,9 @@ fn mismatched_name() -> Result<()> { ----- stderr ----- × No solution found when resolving dependencies: ╰─▶ Because foo was found, but has an invalid format and you require foo, we can conclude that the requirements are unsatisfiable. + + hint: The structure of foo was invalid: + The .dist-info directory tomli-2.0.1 does not start with the normalized package name: foo "### ); From 997f3c9161ac957f00b12b86e2ee5243e67b2d95 Mon Sep 17 00:00:00 2001 From: Aria Beingessner Date: Tue, 9 Apr 2024 23:41:16 -0400 Subject: [PATCH 075/110] chore: update axoupdater to 0.4.0 and add a test (#2938) ## Summary This updates to the version of axoupdater used in cargo-dist 0.13.0's own selfupdate command, with all relevant fixes for platforms. It also tentatively introduces a mildly dangerous self-runtest that runs `uv self update` and checks that the binary is installed and executable. I *believe* some adjustments need to be made to your CI to have this new test run, because it requires the `self-update` feature to be enabled, and I didn't want to just start messing with how you do feature coverage in your CI. **As a result I haven't yet had a chance to actually fully run this in CI**, though I've locally tested it on windows (with the guard disabled). ## Test Plan Most of the machinery here is provided by axoupdater itself (cargo-dist also includes a variant of these tests in its codebase). This initial implementation has a couple major limitations: * This is For Reals modifying the system that runs the test (so it's off unless it detects it's running in CI, and if you want variations on this test they'll need to be [run in serial](https://github.com/axodotdev/cargo-dist/blob/5e7826f7b09a64e5ba9edc16dab8cee7b13daa27/cargo-dist/tests/cli-tests.rs#L235)). Since many of the testing issues were surrounding precise details of Actual Deployed Executions, this seemed worth the tradeoff. * The actual installer *script* it's ultimately invoking is the one you last published, and *not* the one that cargo-dist will make when you next publish. We're already working on implementing some logic for "get cargo-dist to generate a fresh installer script too", which is in fact the basis of a huge amount of cargo-dist's own testsuite. Now that we're dogfooding this stuff, it should be quite hard for this stuff to break without cargo-dist's own codebase noticing it first. --- Cargo.lock | 26 +++++++++++++++++---- Cargo.toml | 2 +- crates/uv/tests/self_update.rs | 41 ++++++++++++++++++++++++++++++++++ 3 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 crates/uv/tests/self_update.rs diff --git a/Cargo.lock b/Cargo.lock index 22ef029e4..5d9293c6a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -295,13 +295,25 @@ dependencies = [ ] [[package]] -name = "axoupdater" -version = "0.3.6" +name = "axotag" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01f40156112045abb1409e115d684306b9ff005399fd10ce2caf33bd800742b4" +checksum = "d888fac0b73e64cbdf36a743fc5a25af5ae955c357535cb420b389bf1e1a6c54" +dependencies = [ + "miette", + "semver", + "thiserror", +] + +[[package]] +name = "axoupdater" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7306cf5b4c3e7037d27ed5e9f7dc1b24d9941c77601e1e24c7d87839df6d8a2" dependencies = [ "axoasset", "axoprocess", + "axotag", "camino", "homedir", "miette", @@ -2667,7 +2679,7 @@ dependencies = [ "indoc", "libc", "memoffset 0.9.0", - "parking_lot 0.11.2", + "parking_lot 0.12.1", "portable-atomic", "pyo3-build-config", "pyo3-ffi", @@ -3337,6 +3349,12 @@ dependencies = [ "libc", ] +[[package]] +name = "semver" +version = "1.0.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92d43fe69e652f3df9bdc2b85b2854a0825b86e4fb76bc44d945137d053639ca" + [[package]] name = "serde" version = "1.0.197" diff --git a/Cargo.toml b/Cargo.toml index 5e4a53df1..fc120dd82 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -57,7 +57,7 @@ async-compression = { version = "0.4.6" } async-trait = { version = "0.1.78" } async_http_range_reader = { version = "0.7.0" } async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "1dcb40cfe1bf5325a6fd4bfcf9894db40241f585", features = ["deflate"] } -axoupdater = { version = "0.3.1", default-features = false } +axoupdater = { version = "0.4.0", default-features = false } backoff = { version = "0.4.0" } base64 = { version = "0.22.0" } cachedir = { version = "0.3.1" } diff --git a/crates/uv/tests/self_update.rs b/crates/uv/tests/self_update.rs new file mode 100644 index 000000000..277f98799 --- /dev/null +++ b/crates/uv/tests/self_update.rs @@ -0,0 +1,41 @@ +#![cfg(feature = "self-update")] + +use crate::common::get_bin; +use axoupdater::{ + test::helpers::{perform_runtest, RuntestArgs}, + ReleaseSourceType, +}; +use std::process::Command; + +mod common; + +#[test] +fn check_self_update() { + // To maximally emulate behaviour in practice, this test actually modifies CARGO_HOME + // and therefore should only be run in CI by default, where it can't hurt developers. + // We use the "CI" env-var that CI machines tend to run + if std::env::var("CI").map(|s| s.is_empty()).unwrap_or(true) { + return; + } + + // Configure the runtest + let args = RuntestArgs { + app_name: "uv".to_owned(), + package: "uv".to_owned(), + owner: "astral-sh".to_owned(), + bin: get_bin(), + binaries: vec!["uv".to_owned()], + args: vec!["self".to_owned(), "update".to_owned()], + release_type: ReleaseSourceType::GitHub, + }; + + // install and update the application + let installed_bin = perform_runtest(&args); + + // check that the binary works like normal + let status = Command::new(installed_bin) + .arg("--version") + .status() + .expect("failed to run 'uv --version'"); + assert!(status.success(), "'uv --version' returned non-zero"); +} From c4472ebbb969d24cb804e2929ba87ff4ac938cda Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 01:13:33 -0400 Subject: [PATCH 076/110] Enforce and backtrack on invalid versions in source metadata (#2954) ## Summary If we build a source distribution from the registry, and the version doesn't match that of the filename, we should error, just as we do for mismatched package names. However, we should also backtrack here, which we didn't previously. Closes https://github.com/astral-sh/uv/issues/2953. ## Test Plan Verified that `cargo run pip install docutils --verbose --no-cache --reinstall` installs `docutils==0.21` instead of the invalid `docutils==0.21.post1`. In the logs, I see: ``` WARN Unable to extract metadata for docutils: Package metadata version `0.21` does not match given version `0.21.post1` ``` --- crates/distribution-types/src/buildable.rs | 10 ++++ crates/uv-distribution/src/error.rs | 3 ++ crates/uv-distribution/src/source/mod.rs | 59 ++++++++++----------- crates/uv-resolver/src/pubgrub/report.rs | 33 ++++++++++++ crates/uv-resolver/src/resolver/mod.rs | 24 +++++++++ crates/uv-resolver/src/resolver/provider.rs | 8 +++ 6 files changed, 105 insertions(+), 32 deletions(-) diff --git a/crates/distribution-types/src/buildable.rs b/crates/distribution-types/src/buildable.rs index 1b105d299..42e7fe789 100644 --- a/crates/distribution-types/src/buildable.rs +++ b/crates/distribution-types/src/buildable.rs @@ -1,6 +1,7 @@ use std::borrow::Cow; use std::path::Path; +use pep440_rs::Version; use url::Url; use uv_normalize::PackageName; @@ -28,6 +29,15 @@ impl BuildableSource<'_> { } } + /// Return the [`Version`] of the source, if available. + pub fn version(&self) -> Option<&Version> { + match self { + Self::Dist(SourceDist::Registry(dist)) => Some(&dist.filename.version), + Self::Dist(_) => None, + Self::Url(_) => None, + } + } + /// Return the [`BuildableSource`] as a [`SourceDist`], if it is a distribution. pub fn as_dist(&self) -> Option<&SourceDist> { match self { diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index fe61b95da..42bf4f48b 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -3,6 +3,7 @@ use tokio::task::JoinError; use zip::result::ZipError; use distribution_filename::WheelFilenameError; +use pep440_rs::Version; use uv_client::BetterReqwestError; use uv_normalize::PackageName; @@ -47,6 +48,8 @@ pub enum Error { given: PackageName, metadata: PackageName, }, + #[error("Package metadata version `{metadata}` does not match given version `{given}`")] + VersionMismatch { given: Version, metadata: Version }, #[error("Failed to parse metadata from built wheel")] Metadata(#[from] pypi_types::MetadataError), #[error("Failed to read `dist-info` metadata from built wheel")] diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 34b52c105..62ccd8c74 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1109,14 +1109,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let metadata = read_wheel_metadata(&filename, cache_shard.join(&disk_filename))?; // Validate the metadata. - if let Some(name) = source.name() { - if metadata.name != *name { - return Err(Error::NameMismatch { - metadata: metadata.name, - given: name.clone(), - }); - } - } + validate(source, &metadata)?; debug!("Finished building: {source}"); Ok((disk_filename, filename, metadata)) @@ -1138,14 +1131,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Found static `PKG-INFO` for: {source}"); // Validate the metadata. - if let Some(name) = source.name() { - if metadata.name != *name { - return Err(Error::NameMismatch { - metadata: metadata.name, - given: name.clone(), - }); - } - } + validate(source, &metadata)?; return Ok(Some(metadata)); } @@ -1161,14 +1147,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { debug!("Found static `pyproject.toml` for: {source}"); // Validate the metadata. - if let Some(name) = source.name() { - if metadata.name != *name { - return Err(Error::NameMismatch { - metadata: metadata.name, - given: name.clone(), - }); - } - } + validate(source, &metadata)?; return Ok(Some(metadata)); } @@ -1208,14 +1187,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let metadata = Metadata23::parse_metadata(&content)?; // Validate the metadata. - if let Some(name) = source.name() { - if metadata.name != *name { - return Err(Error::NameMismatch { - metadata: metadata.name, - given: name.clone(), - }); - } - } + validate(source, &metadata)?; Ok(Some(metadata)) } @@ -1278,6 +1250,29 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } +/// Validate that the source distribution matches the built metadata. +fn validate(source: &BuildableSource<'_>, metadata: &Metadata23) -> Result<(), Error> { + if let Some(name) = source.name() { + if metadata.name != *name { + return Err(Error::NameMismatch { + metadata: metadata.name.clone(), + given: name.clone(), + }); + } + } + + if let Some(version) = source.version() { + if metadata.version != *version { + return Err(Error::VersionMismatch { + metadata: metadata.version.clone(), + given: version.clone(), + }); + } + } + + Ok(()) +} + /// Read an existing HTTP-cached [`Revision`], if it exists. pub(crate) fn read_http_revision(cache_entry: &CacheEntry) -> Result, Error> { match fs_err::File::open(cache_entry.path()) { diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs index da63129ef..f4c7aea9e 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -446,6 +446,15 @@ impl PubGrubReportFormatter<'_> { reason: reason.clone(), }); } + IncompletePackage::InconsistentMetadata(reason) => { + hints.insert( + PubGrubHint::InconsistentVersionMetadata { + package: package.clone(), + version: version.clone(), + reason: reason.clone(), + }, + ); + } IncompletePackage::InvalidStructure(reason) => { hints.insert( PubGrubHint::InvalidVersionStructure { @@ -530,6 +539,15 @@ pub(crate) enum PubGrubHint { #[derivative(PartialEq = "ignore", Hash = "ignore")] reason: String, }, + /// Metadata for a package version was inconsistent (e.g., the package name did not match that + /// of the file). + InconsistentVersionMetadata { + package: PubGrubPackage, + #[derivative(PartialEq = "ignore", Hash = "ignore")] + version: Version, + #[derivative(PartialEq = "ignore", Hash = "ignore")] + reason: String, + }, /// The structure of a package version was invalid (e.g., multiple `.dist-info` directories). InvalidVersionStructure { package: PubGrubPackage, @@ -629,6 +647,21 @@ impl std::fmt::Display for PubGrubHint { textwrap::indent(reason, " ") ) } + PubGrubHint::InconsistentVersionMetadata { + package, + version, + reason, + } => { + write!( + f, + "{}{} Metadata for {}=={} was inconsistent:\n{}", + "hint".bold().cyan(), + ":".bold(), + package.bold(), + version.bold(), + textwrap::indent(reason, " ") + ) + } } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 2f2795c03..5e3723f4c 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -93,6 +93,8 @@ pub(crate) enum IncompletePackage { Offline, /// The wheel metadata was found, but could not be parsed. InvalidMetadata(String), + /// The wheel metadata was found, but the metadata was inconsistent. + InconsistentMetadata(String), /// The wheel has an invalid structure. InvalidStructure(String), } @@ -646,6 +648,13 @@ impl< ); return Ok(None); } + MetadataResponse::InconsistentMetadata(err) => { + self.unavailable_packages.insert( + package_name.clone(), + UnavailablePackage::InvalidMetadata(err.to_string()), + ); + return Ok(None); + } MetadataResponse::InvalidStructure(err) => { self.unavailable_packages.insert( package_name.clone(), @@ -945,6 +954,7 @@ impl< )); } MetadataResponse::InvalidMetadata(err) => { + warn!("Unable to extract metadata for {package_name}: {err}"); self.incomplete_packages .entry(package_name.clone()) .or_default() @@ -956,7 +966,21 @@ impl< "the package metadata could not be parsed".to_string(), )); } + MetadataResponse::InconsistentMetadata(err) => { + warn!("Unable to extract metadata for {package_name}: {err}"); + self.incomplete_packages + .entry(package_name.clone()) + .or_default() + .insert( + version.clone(), + IncompletePackage::InconsistentMetadata(err.to_string()), + ); + return Ok(Dependencies::Unavailable( + "the package metadata was inconsistent".to_string(), + )); + } MetadataResponse::InvalidStructure(err) => { + warn!("Unable to extract metadata for {package_name}: {err}"); self.incomplete_packages .entry(package_name.clone()) .or_default() diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 10c89a001..c778e780e 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -38,6 +38,8 @@ pub enum MetadataResponse { Found(Metadata23), /// The wheel metadata was found, but could not be parsed. InvalidMetadata(Box), + /// The wheel metadata was found, but the metadata was inconsistent. + InconsistentMetadata(Box), /// The wheel has an invalid structure. InvalidStructure(Box), /// The wheel metadata was not found in the cache and the network is not available. @@ -185,6 +187,12 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider } kind => Err(uv_client::Error::from(kind).into()), }, + uv_distribution::Error::VersionMismatch { .. } => { + Ok(MetadataResponse::InconsistentMetadata(Box::new(err))) + } + uv_distribution::Error::NameMismatch { .. } => { + Ok(MetadataResponse::InconsistentMetadata(Box::new(err))) + } uv_distribution::Error::Metadata(err) => { Ok(MetadataResponse::InvalidMetadata(Box::new(err))) } From 15f0be8f04db17c9419e0645c210a0efa037a4a3 Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 10 Apr 2024 12:15:27 +0200 Subject: [PATCH 077/110] Allow profiling tests with tracing instrumentation (#2957) To get more insights into test performance, allow instrumenting tests with tracing-durations-export. Usage: ```shell # A single test TRACING_DURATIONS_TEST_ROOT=$(pwd)/target/test-traces cargo test --features tracing-durations-export --test pip_install_scenarios no_binary -- --exact # All tests TRACING_DURATIONS_TEST_ROOT=$(pwd)/target/test-traces cargo nextest run --features tracing-durations-export ``` Then we can e.g. look at `target/test-traces/pip_install_scenarios::no_binary.svg` and see the builds it performs: ![image](https://github.com/astral-sh/uv/assets/6826232/40b4e094-debc-4b22-8aa3-9471998674af) --- Cargo.toml | 2 +- crates/uv-build/src/lib.rs | 4 +-- crates/uv-dev/src/build.rs | 2 +- crates/uv-distribution/src/source/mod.rs | 4 +-- crates/uv/src/logging.rs | 36 +++++++++++++---------- crates/uv/src/main.rs | 2 +- crates/uv/tests/common/mod.rs | 37 ++++++++++++++++++++++-- 7 files changed, 63 insertions(+), 24 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fc120dd82..52f2123a5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -134,7 +134,7 @@ toml = { version = "0.8.12" } tracing = { version = "0.1.40" } tracing-durations-export = { version = "0.2.0", features = ["plot"] } tracing-indicatif = { version = "0.3.6" } -tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json"] } +tracing-subscriber = { version = "0.3.18", features = ["env-filter", "json", "registry"] } tracing-tree = { version = "0.3.0" } unicode-width = { version = "0.1.11" } unscanny = { version = "0.1.0" } diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index 6c914de66..5933511aa 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -715,7 +715,7 @@ impl SourceBuild { /// /// #[instrument(skip_all, fields(package_id = self.package_id))] - pub async fn build(&self, wheel_dir: &Path) -> Result { + pub async fn build_wheel(&self, wheel_dir: &Path) -> Result { // The build scripts run with the extracted root as cwd, so they need the absolute path. let wheel_dir = fs::canonicalize(wheel_dir)?; @@ -856,7 +856,7 @@ impl SourceBuildTrait for SourceBuild { } async fn wheel<'a>(&'a self, wheel_dir: &'a Path) -> anyhow::Result { - Ok(self.build(wheel_dir).await?) + Ok(self.build_wheel(wheel_dir).await?) } } diff --git a/crates/uv-dev/src/build.rs b/crates/uv-dev/src/build.rs index 20430e1a0..60457a170 100644 --- a/crates/uv-dev/src/build.rs +++ b/crates/uv-dev/src/build.rs @@ -91,5 +91,5 @@ pub(crate) async fn build(args: BuildArgs) -> Result { FxHashMap::default(), ) .await?; - Ok(wheel_dir.join(builder.build(&wheel_dir).await?)) + Ok(wheel_dir.join(builder.build_wheel(&wheel_dir).await?)) } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 62ccd8c74..dab44f313 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -1063,7 +1063,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { /// Build a source distribution, storing the built wheel in the cache. /// /// Returns the un-normalized disk filename, the parsed, normalized filename and the metadata - #[instrument(skip_all, fields(dist))] + #[instrument(skip_all, fields(dist = %source))] async fn build_distribution( &self, source: &BuildableSource<'_>, @@ -1116,7 +1116,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } /// Build the metadata for a source distribution. - #[instrument(skip_all, fields(dist))] + #[instrument(skip_all, fields(dist = %source))] async fn build_metadata( &self, source: &BuildableSource<'_>, diff --git a/crates/uv/src/logging.rs b/crates/uv/src/logging.rs index 6487b0fb6..3ba021d59 100644 --- a/crates/uv/src/logging.rs +++ b/crates/uv/src/logging.rs @@ -5,7 +5,6 @@ use std::str::FromStr; use anyhow::Context; use chrono::Utc; use owo_colors::OwoColorize; -use tracing::level_filters::LevelFilter; use tracing::{Event, Subscriber}; #[cfg(feature = "tracing-durations-export")] use tracing_durations_export::{ @@ -115,12 +114,12 @@ where /// includes targets and timestamps, along with all `uv=debug` messages by default. pub(crate) fn setup_logging( level: Level, - duration: impl Layer + Send + Sync, + durations: impl Layer + Send + Sync, ) -> anyhow::Result<()> { let default_directive = match level { Level::Default => { // Show nothing, but allow `RUST_LOG` to override. - LevelFilter::OFF.into() + tracing::level_filters::LevelFilter::OFF.into() } Level::Verbose | Level::ExtraVerbose => { // Show `DEBUG` messages from the CLI crate, but allow `RUST_LOG` to override. @@ -128,6 +127,13 @@ pub(crate) fn setup_logging( } }; + // Only record our own spans. + let durations_layer = + durations.with_filter(tracing_subscriber::filter::Targets::new().with_target( + env!("CARGO_PKG_NAME"), + tracing::level_filters::LevelFilter::INFO, + )); + let filter = EnvFilter::builder() .with_default_directive(default_directive) .from_env() @@ -148,26 +154,26 @@ pub(crate) fn setup_logging( ColorChoice::Auto => unreachable!(), }; tracing_subscriber::registry() - .with(duration) - .with(filter) + .with(durations_layer) .with( tracing_subscriber::fmt::layer() .event_format(format) .with_writer(std::io::stderr) - .with_ansi(ansi), + .with_ansi(ansi) + .with_filter(filter), ) .init(); } Level::ExtraVerbose => { // Regardless of the tracing level, include the uptime and target for each message. tracing_subscriber::registry() - .with(duration) - .with(filter) + .with(durations_layer) .with( HierarchicalLayer::default() .with_targets(true) .with_timer(Uptime::default()) - .with_writer(std::io::stderr), + .with_writer(std::io::stderr) + .with_filter(filter), ) .init(); } @@ -178,15 +184,15 @@ pub(crate) fn setup_logging( /// Setup the `TRACING_DURATIONS_FILE` environment variable to enable tracing durations. #[cfg(feature = "tracing-durations-export")] -pub(crate) fn setup_duration() -> ( +pub(crate) fn setup_duration() -> anyhow::Result<( Option>, Option, -) { +)> { if let Ok(location) = std::env::var("TRACING_DURATIONS_FILE") { let location = std::path::PathBuf::from(location); if let Some(parent) = location.parent() { fs_err::create_dir_all(parent) - .expect("Failed to create parent of TRACING_DURATIONS_FILE"); + .context("Failed to create parent of TRACING_DURATIONS_FILE")?; } let plot_config = PlotConfig { multi_lane: true, @@ -203,9 +209,9 @@ pub(crate) fn setup_duration() -> ( .plot_file(location.with_extension("svg")) .plot_config(plot_config) .build() - .expect("Couldn't create TRACING_DURATIONS_FILE files"); - (Some(layer), Some(guard)) + .context("Couldn't create TRACING_DURATIONS_FILE files")?; + Ok((Some(layer), Some(guard))) } else { - (None, None) + Ok((None, None)) } } diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 134e11a30..22f318c7f 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -1467,7 +1467,7 @@ async fn run() -> Result { // Configure the `tracing` crate, which controls internal logging. #[cfg(feature = "tracing-durations-export")] - let (duration_layer, _duration_guard) = logging::setup_duration(); + let (duration_layer, _duration_guard) = logging::setup_duration()?; #[cfg(not(feature = "tracing-durations-export"))] let duration_layer = None::; logging::setup_logging( diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 5a46a956f..251331a83 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -450,6 +450,7 @@ pub fn create_bin_with_executables( pub fn run_and_format>( mut command: impl BorrowMut, filters: impl AsRef<[(T, T)]>, + function_name: &str, windows_filters: bool, ) -> (String, Output) { let program = command @@ -457,6 +458,19 @@ pub fn run_and_format>( .get_program() .to_string_lossy() .to_string(); + + // Support profiling test run commands with traces. + if let Ok(root) = env::var("TRACING_DURATIONS_TEST_ROOT") { + assert!( + cfg!(feature = "tracing-durations-export"), + "You need to enable the tracing-durations-export feature to use `TRACING_DURATIONS_TEST_ROOT`" + ); + command.borrow_mut().env( + "TRACING_DURATIONS_FILE", + Path::new(&root).join(function_name).with_extension("jsonl"), + ); + } + let output = command .borrow_mut() .output() @@ -527,6 +541,25 @@ pub fn copy_dir_all(src: impl AsRef, dst: impl AsRef) -> std::io::Re Ok(()) } +/// Utility macro to return the name of the current function. +/// +/// https://stackoverflow.com/a/40234666/3549270 +#[doc(hidden)] +#[macro_export] +macro_rules! function_name { + () => {{ + fn f() {} + fn type_name_of_val(_: T) -> &'static str { + std::any::type_name::() + } + let mut name = type_name_of_val(f).strip_suffix("::f").unwrap_or(""); + while let Some(rest) = name.strip_suffix("::{{closure}}") { + name = rest; + } + name + }}; +} + /// Run [`assert_cmd_snapshot!`], with default filters or with custom filters. /// /// By default, the filters will search for the generally windows-only deps colorama and tzdata, @@ -538,13 +571,13 @@ macro_rules! uv_snapshot { }}; ($filters:expr, $spawnable:expr, @$snapshot:literal) => {{ // Take a reference for backwards compatibility with the vec-expecting insta filters. - let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, true); + let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, function_name!(), true); ::insta::assert_snapshot!(snapshot, @$snapshot); output }}; ($filters:expr, windows_filters=false, $spawnable:expr, @$snapshot:literal) => {{ // Take a reference for backwards compatibility with the vec-expecting insta filters. - let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, false); + let (snapshot, output) = $crate::common::run_and_format($spawnable, &$filters, function_name!(), false); ::insta::assert_snapshot!(snapshot, @$snapshot); output }}; From 273de456eac2f69e85b31906d4407514838500b3 Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 10 Apr 2024 15:26:18 +0200 Subject: [PATCH 078/110] Remove rust 1.75 workaround (#2959) --- crates/uv-dispatch/src/lib.rs | 186 +++++++++++++++++----------------- 1 file changed, 91 insertions(+), 95 deletions(-) diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 52f1cdda2..072db577a 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -3,8 +3,8 @@ //! implementing [`BuildContext`]. use std::ffi::OsStr; +use std::ffi::OsString; use std::path::Path; -use std::{ffi::OsString, future::Future}; use anyhow::{bail, Context, Result}; use futures::FutureExt; @@ -155,7 +155,6 @@ impl<'a> BuildContext for BuildDispatch<'a> { Ok(Resolution::from(graph)) } - #[allow(clippy::manual_async_fn)] // TODO(konstin): rustc 1.75 gets into a type inference cycle with async fn #[instrument( skip(self, resolution, venv), fields( @@ -163,113 +162,110 @@ impl<'a> BuildContext for BuildDispatch<'a> { venv = ?venv.root() ) )] - fn install<'data>( + async fn install<'data>( &'data self, resolution: &'data Resolution, venv: &'data PythonEnvironment, - ) -> impl Future> + Send + 'data { - async move { - debug!( - "Installing in {} in {}", + ) -> Result<()> { + debug!( + "Installing in {} in {}", + resolution + .distributions() + .map(ToString::to_string) + .join(", "), + venv.root().display(), + ); + + // Determine the current environment markers. + let tags = self.interpreter.tags()?; + + // Determine the set of installed packages. + let site_packages = SitePackages::from_executable(venv)?; + + let Plan { + cached, + remote, + installed: _, + reinstalls, + extraneous: _, + } = Planner::with_requirements(&resolution.requirements()).build( + site_packages, + &Reinstall::None, + &NoBinary::None, + self.index_locations, + self.cache(), + venv, + tags, + )?; + + // Nothing to do. + if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() { + debug!("No build requirements to install for build"); + return Ok(()); + } + + // Resolve any registry-based requirements. + let remote = remote + .iter() + .map(|dist| { resolution - .distributions() - .map(ToString::to_string) - .join(", "), - venv.root().display(), + .get_remote(&dist.name) + .cloned() + .expect("Resolution should contain all packages") + }) + .collect::>(); + + // Download any missing distributions. + let wheels = if remote.is_empty() { + vec![] + } else { + // TODO(konstin): Check that there is no endless recursion. + let downloader = Downloader::new(self.cache, tags, self.client, self); + debug!( + "Downloading and building requirement{} for build: {}", + if remote.len() == 1 { "" } else { "s" }, + remote.iter().map(ToString::to_string).join(", ") ); - // Determine the current environment markers. - let tags = self.interpreter.tags()?; + downloader + .download(remote, self.in_flight) + .await + .context("Failed to download and build distributions")? + }; - // Determine the set of installed packages. - let site_packages = SitePackages::from_executable(venv)?; - - let Plan { - cached, - remote, - installed: _, - reinstalls, - extraneous: _, - } = Planner::with_requirements(&resolution.requirements()).build( - site_packages, - &Reinstall::None, - &NoBinary::None, - self.index_locations, - self.cache(), - venv, - tags, - )?; - - // Nothing to do. - if remote.is_empty() && cached.is_empty() && reinstalls.is_empty() { - debug!("No build requirements to install for build"); - return Ok(()); - } - - // Resolve any registry-based requirements. - let remote = remote - .iter() - .map(|dist| { - resolution - .get_remote(&dist.name) - .cloned() - .expect("Resolution should contain all packages") - }) - .collect::>(); - - // Download any missing distributions. - let wheels = if remote.is_empty() { - vec![] - } else { - // TODO(konstin): Check that there is no endless recursion. - let downloader = Downloader::new(self.cache, tags, self.client, self); - debug!( - "Downloading and building requirement{} for build: {}", - if remote.len() == 1 { "" } else { "s" }, - remote.iter().map(ToString::to_string).join(", ") - ); - - downloader - .download(remote, self.in_flight) + // Remove any unnecessary packages. + if !reinstalls.is_empty() { + for dist_info in &reinstalls { + let summary = uv_installer::uninstall(dist_info) .await - .context("Failed to download and build distributions")? - }; - - // Remove any unnecessary packages. - if !reinstalls.is_empty() { - for dist_info in &reinstalls { - let summary = uv_installer::uninstall(dist_info) - .await - .context("Failed to uninstall build dependencies")?; - debug!( - "Uninstalled {} ({} file{}, {} director{})", - dist_info.name(), - summary.file_count, - if summary.file_count == 1 { "" } else { "s" }, - summary.dir_count, - if summary.dir_count == 1 { "y" } else { "ies" }, - ); - } - } - - // Install the resolved distributions. - let wheels = wheels.into_iter().chain(cached).collect::>(); - if !wheels.is_empty() { + .context("Failed to uninstall build dependencies")?; debug!( - "Installing build requirement{}: {}", - if wheels.len() == 1 { "" } else { "s" }, - wheels.iter().map(ToString::to_string).join(", ") + "Uninstalled {} ({} file{}, {} director{})", + dist_info.name(), + summary.file_count, + if summary.file_count == 1 { "" } else { "s" }, + summary.dir_count, + if summary.dir_count == 1 { "y" } else { "ies" }, ); - Installer::new(venv) - .install(&wheels) - .context("Failed to install build dependencies")?; } - - Ok(()) } + + // Install the resolved distributions. + let wheels = wheels.into_iter().chain(cached).collect::>(); + if !wheels.is_empty() { + debug!( + "Installing build requirement{}: {}", + if wheels.len() == 1 { "" } else { "s" }, + wheels.iter().map(ToString::to_string).join(", ") + ); + Installer::new(venv) + .install(&wheels) + .context("Failed to install build dependencies")?; + } + + Ok(()) } - #[allow(clippy::manual_async_fn)] // TODO(konstin): rustc 1.75 gets into a type inference cycle with async fn #[instrument(skip_all, fields(package_id = package_id, subdirectory = ?subdirectory))] async fn setup_build<'data>( &'data self, From c345a79b9b5db77e31bc092a6640267cb547311c Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 10 Apr 2024 09:01:25 -0500 Subject: [PATCH 079/110] Add `python-patch` feature to isolate tests that require Python patch versions to match our suite (#2940) Closes https://github.com/astral-sh/uv/issues/2165 Follows https://github.com/astral-sh/uv/pull/2930 --- crates/uv/Cargo.toml | 4 +++- crates/uv/tests/pip_compile_scenarios.rs | 2 ++ crates/uv/tests/pip_install_scenarios.rs | 5 +++-- crates/uv/tests/venv.rs | 1 + scripts/scenarios/generate.py | 7 +++++++ scripts/scenarios/templates/compile.mustache | 3 +++ scripts/scenarios/templates/install.mustache | 3 +++ 7 files changed, 22 insertions(+), 3 deletions(-) diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 83588c1b0..efe192180 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -82,9 +82,11 @@ regex = { version = "1.10.3" } reqwest = { version = "0.11.23", features = ["blocking"], default-features = false } [features] -default = ["flate2/zlib-ng", "python", "pypi", "git", "maturin"] +default = ["flate2/zlib-ng", "python", "pypi", "git", "maturin", "python-patch"] # Introduces a dependency on a local Python installation. python = [] +# Introduces a dependency on a local Python installation with specific patch versions. +python-patch = [] # Introduces a dependency on PyPI. pypi = [] # Introduces a dependency on Git. diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index 222467a19..a0fb32102 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -386,6 +386,7 @@ fn incompatible_python_compatible_override_other_wheel() -> Result<()> { /// └── a-1.0.0 /// └── requires python>=3.8.4 /// ``` +#[cfg(feature = "python-patch")] #[test] fn python_patch_override_no_patch() -> Result<()> { let context = TestContext::new("3.8.18"); @@ -433,6 +434,7 @@ fn python_patch_override_no_patch() -> Result<()> { /// └── a-1.0.0 /// └── requires python>=3.8.0 /// ``` +#[cfg(feature = "python-patch")] #[test] fn python_patch_override_patch_compatible() -> Result<()> { let context = TestContext::new("3.8.18"); diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 0f17c4612..5c17e809d 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -501,7 +501,7 @@ fn dependency_excludes_range_of_compatible_versions() { /// 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 +/// `a` are incompatible for another reason e.g. dependency on non-existant package /// `d`. /// /// ```text @@ -3485,7 +3485,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 + // allowed. Since the user did not explictly request a pre-release, pre-releases at // the boundary should not be selected. assert_installed( &context.venv, @@ -3780,6 +3780,7 @@ fn python_greater_than_current() { /// └── a-1.0.0 /// └── requires python>=3.8.14 (incompatible with environment) /// ``` +#[cfg(feature = "python-patch")] #[test] fn python_greater_than_current_patch() { let context = TestContext::new("3.8.12"); diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/venv.rs index 282ad0370..c30c4e00b 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/venv.rs @@ -281,6 +281,7 @@ fn create_venv_unknown_python_patch() { context.venv.assert(predicates::path::missing()); } +#[cfg(feature = "python-patch")] #[test] fn create_venv_python_patch() { let context = VenvTestContext::new(&["3.12.1"]); diff --git a/scripts/scenarios/generate.py b/scripts/scenarios/generate.py index 37bd180e3..207ba3379 100755 --- a/scripts/scenarios/generate.py +++ b/scripts/scenarios/generate.py @@ -133,6 +133,13 @@ def main(scenarios: list[Path], snapshot_update: bool = True): else [] ) + # Hack to track which scenarios require a specific Python patch version + for scenario in data["scenarios"]: + if "patch" in scenario["name"]: + scenario["python_patch"] = True + else: + scenario["python_patch"] = False + # We don't yet support local versions that aren't expressed as direct dependencies. for scenario in data["scenarios"]: expected = scenario["expected"] diff --git a/scripts/scenarios/templates/compile.mustache b/scripts/scenarios/templates/compile.mustache index 0914fc04a..1e019b656 100644 --- a/scripts/scenarios/templates/compile.mustache +++ b/scripts/scenarios/templates/compile.mustache @@ -58,6 +58,9 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command { /// {{.}} {{/tree}} /// ``` +{{#python_patch}} +#[cfg(feature = "python-patch")] +{{/python_patch}} #[test] fn {{module_name}}() -> Result<()> { let context = TestContext::new("{{environment.python}}"); diff --git a/scripts/scenarios/templates/install.mustache b/scripts/scenarios/templates/install.mustache index 0aa0168b7..51e309ea0 100644 --- a/scripts/scenarios/templates/install.mustache +++ b/scripts/scenarios/templates/install.mustache @@ -77,6 +77,9 @@ fn command(context: &TestContext) -> Command { /// {{.}} {{/tree}} /// ``` +{{#python_patch}} +#[cfg(feature = "python-patch")] +{{/python_patch}} #[test] fn {{module_name}}() { let context = TestContext::new("{{environment.python}}"); From 38ab39c439ca358a836e2033c3d3a69d8f5e03e7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 10:25:29 -0400 Subject: [PATCH 080/110] Strip query string when parsing filename from HTML index (#2961) ## Summary Closes https://github.com/astral-sh/uv/issues/2958. --- crates/uv-client/src/html.rs | 57 ++++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/crates/uv-client/src/html.rs b/crates/uv-client/src/html.rs index 2bffce9b6..5d315c6d4 100644 --- a/crates/uv-client/src/html.rs +++ b/crates/uv-client/src/html.rs @@ -164,6 +164,9 @@ impl SimpleHtml { .last() .ok_or_else(|| Error::MissingFilename(href.to_string()))?; + // Strip any query string from the filename. + let filename = filename.split('?').next().unwrap_or(filename); + // Unquote the filename. let filename = urlencoding::decode(filename) .map_err(|_| Error::UnsupportedFilename(filename.to_string()))?; @@ -681,6 +684,60 @@ mod tests { "###); } + #[test] + fn parse_query_string() { + let text = r#" + + + +

Links for jinja2

+ Jinja2-3.1.2-py3-none-any.whl
+ + + + "#; + let base = Url::parse("https://download.pytorch.org/whl/jinja2/").unwrap(); + let result = SimpleHtml::parse(text, &base).unwrap(); + insta::assert_debug_snapshot!(result, @r###" + SimpleHtml { + base: BaseUrl( + Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), + ), + port: None, + path: "/whl/jinja2/", + query: None, + fragment: None, + }, + ), + files: [ + File { + dist_info_metadata: None, + filename: "Jinja2-3.1.2-py3-none-any.whl", + hashes: Hashes { + md5: None, + sha256: None, + sha384: None, + sha512: None, + }, + requires_python: None, + size: None, + upload_time: None, + url: "/whl/Jinja2-3.1.2-py3-none-any.whl?project=legacy", + yanked: None, + }, + ], + } + "###); + } + #[test] fn parse_missing_hash_value() { let text = r#" From bbe46c074c692dd973422a03c2f5a8c5a2adc515 Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 10 Apr 2024 09:30:57 -0500 Subject: [PATCH 081/110] Upgrade packse (#2963) Should improve test performance with https://github.com/astral-sh/packse/pull/169 thanks @konstin ! --- crates/uv/tests/pip_compile_scenarios.rs | 6 +++--- crates/uv/tests/pip_install_scenarios.rs | 8 ++++---- scripts/scenarios/requirements.txt | 26 ++++++++++++++---------- 3 files changed, 22 insertions(+), 18 deletions(-) diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index a0fb32102..91d563ebf 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] @@ -27,9 +27,9 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command { .arg("compile") .arg("requirements.in") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.12/simple-html/") + .arg("https://astral-sh.github.io/packse/0.3.13/simple-html/") .arg("--find-links") - .arg("https://raw.githubusercontent.com/zanieb/packse/0.3.12/vendor/links.html") + .arg("https://raw.githubusercontent.com/zanieb/packse/0.3.13/vendor/links.html") .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 5c17e809d..001cba3c1 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] @@ -46,9 +46,9 @@ fn command(context: &TestContext) -> Command { .arg("pip") .arg("install") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.12/simple-html/") + .arg("https://astral-sh.github.io/packse/0.3.13/simple-html/") .arg("--find-links") - .arg("https://raw.githubusercontent.com/zanieb/packse/0.3.12/vendor/links.html") + .arg("https://raw.githubusercontent.com/zanieb/packse/0.3.13/vendor/links.html") .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) @@ -1752,7 +1752,7 @@ fn local_transitive_confounding() { And because only package-a==1.0.0 is available and you require package-a, we can conclude that the requirements are unsatisfiable. "###); - // The version '1.2.3+foo' satisfies the constraint '==1.2.3'. + // The version '2.0.0+foo' satisfies the constraint '==2.0.0'. assert_not_installed( &context.venv, "local_transitive_confounding_a", diff --git a/scripts/scenarios/requirements.txt b/scripts/scenarios/requirements.txt index c4dbcd759..7a4cc2244 100644 --- a/scripts/scenarios/requirements.txt +++ b/scripts/scenarios/requirements.txt @@ -6,33 +6,37 @@ charset-normalizer==3.3.2 # via requests chevron-blue==0.2.1 # via packse -docutils==0.20.1 +docutils==0.21.post1 # via readme-renderer -editables==0.5 - # via hatchling -hatchling==1.21.1 +hatchling==1.22.5 # via packse idna==3.6 # via requests -importlib-metadata==7.0.2 +importlib-metadata==7.1.0 # via twine -jaraco-classes==3.3.1 +jaraco-classes==3.4.0 # via keyring -keyring==24.3.1 +jaraco-context==5.3.0 + # via keyring +jaraco-functools==4.0.0 + # via keyring +keyring==25.1.0 # via twine markdown-it-py==3.0.0 # via rich mdurl==0.1.2 # via markdown-it-py more-itertools==10.2.0 - # via jaraco-classes + # via + # jaraco-classes + # jaraco-functools msgspec==0.18.6 # via packse -nh3==0.2.15 +nh3==0.2.17 # via readme-renderer packaging==24.0 # via hatchling -packse==0.3.12 +packse==0.3.13 pathspec==0.12.1 # via hatchling pkginfo==1.10.0 @@ -57,7 +61,7 @@ rich==13.7.1 # via twine setuptools==69.2.0 # via packse -trove-classifiers==2024.3.3 +trove-classifiers==2024.4.10 # via hatchling twine==4.0.2 # via packse From d8323551f8dae33ec1efa2b0e1d1994fce37a0cd Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 10:56:34 -0400 Subject: [PATCH 082/110] Update hashes without `--upgrade` if not present (#2966) ## Summary If the user runs with `--generate-hashes`, and the lockfile doesn't contain _any_ hashes for a package (despite being pinned), we should add new hashes. This mirrors running `uv pip compile --generate-hashes` for the first time with an existing lockfile. Closes #2962. --- crates/uv-resolver/src/resolution.rs | 26 +++++---- crates/uv/tests/pip_compile.rs | 87 ++++++++++++++++++++++++++++ 2 files changed, 103 insertions(+), 10 deletions(-) diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index da93cce16..fe81b49b3 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -96,14 +96,17 @@ impl ResolutionGraph { // Add its hashes to the index, preserving those that were already present in // the lockfile if necessary. - if let Some(hash) = preferences.match_hashes(package_name, version) { - hashes.insert(package_name.clone(), hash.to_vec()); + if let Some(digests) = preferences + .match_hashes(package_name, version) + .filter(|digests| !digests.is_empty()) + { + hashes.insert(package_name.clone(), digests.to_vec()); } else if let Some(versions_response) = packages.get(package_name) { if let VersionsResponse::Found(ref version_maps) = *versions_response { for version_map in version_maps { - if let Some(mut hash) = version_map.hashes(version) { - hash.sort_unstable(); - hashes.insert(package_name.clone(), hash); + if let Some(mut digests) = version_map.hashes(version) { + digests.sort_unstable(); + hashes.insert(package_name.clone(), digests); break; } } @@ -126,14 +129,17 @@ impl ResolutionGraph { // Add its hashes to the index, preserving those that were already present in // the lockfile if necessary. - if let Some(hash) = preferences.match_hashes(package_name, version) { - hashes.insert(package_name.clone(), hash.to_vec()); + if let Some(digests) = preferences + .match_hashes(package_name, version) + .filter(|digests| !digests.is_empty()) + { + hashes.insert(package_name.clone(), digests.to_vec()); } else if let Some(versions_response) = packages.get(package_name) { if let VersionsResponse::Found(ref version_maps) = *versions_response { for version_map in version_maps { - if let Some(mut hash) = version_map.hashes(version) { - hash.sort_unstable(); - hashes.insert(package_name.clone(), hash); + if let Some(mut digests) = version_map.hashes(version) { + digests.sort_unstable(); + hashes.insert(package_name.clone(), digests); break; } } diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 4bff90244..79281c57a 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -6298,6 +6298,93 @@ fn preserve_hashes_upgrade() -> Result<()> { Ok(()) } +/// `--generate-hashes` should update the hashes in the "lockfile" if the package does not have +/// hashes, even if `--upgrade` is _not_ specified. +#[test] +fn preserve_hashes_no_existing_hashes() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("markupsafe")?; + + // Write a subset of the hashes to the "lockfile". + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc! {r" + # This file was autogenerated by uv via the following command: + # uv pip compile requirements.in --python-version 3.12 --cache-dir [CACHE_DIR] + markupsafe==2.1.2 + "})?; + + // Add additional hashes to the "lockfile". + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--output-file") + .arg("requirements.txt") + .arg("--generate-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --output-file requirements.txt --generate-hashes + markupsafe==2.1.2 \ + --hash=sha256:0576fe974b40a400449768941d5d0858cc624e3249dfd1e0c33674e5c7ca7aed \ + --hash=sha256:085fd3201e7b12809f9e6e9bc1e5c96a368c8523fad5afb02afe3c051ae4afcc \ + --hash=sha256:090376d812fb6ac5f171e5938e82e7f2d7adc2b629101cec0db8b267815c85e2 \ + --hash=sha256:0b462104ba25f1ac006fdab8b6a01ebbfbce9ed37fd37fd4acd70c67c973e460 \ + --hash=sha256:137678c63c977754abe9086a3ec011e8fd985ab90631145dfb9294ad09c102a7 \ + --hash=sha256:1bea30e9bf331f3fef67e0a3877b2288593c98a21ccb2cf29b74c581a4eb3af0 \ + --hash=sha256:22152d00bf4a9c7c83960521fc558f55a1adbc0631fbb00a9471e097b19d72e1 \ + --hash=sha256:22731d79ed2eb25059ae3df1dfc9cb1546691cc41f4e3130fe6bfbc3ecbbecfa \ + --hash=sha256:2298c859cfc5463f1b64bd55cb3e602528db6fa0f3cfd568d3605c50678f8f03 \ + --hash=sha256:28057e985dace2f478e042eaa15606c7efccb700797660629da387eb289b9323 \ + --hash=sha256:2e7821bffe00aa6bd07a23913b7f4e01328c3d5cc0b40b36c0bd81d362faeb65 \ + --hash=sha256:2ec4f2d48ae59bbb9d1f9d7efb9236ab81429a764dedca114f5fdabbc3788013 \ + --hash=sha256:340bea174e9761308703ae988e982005aedf427de816d1afe98147668cc03036 \ + --hash=sha256:40627dcf047dadb22cd25ea7ecfe9cbf3bbbad0482ee5920b582f3809c97654f \ + --hash=sha256:40dfd3fefbef579ee058f139733ac336312663c6706d1163b82b3003fb1925c4 \ + --hash=sha256:4cf06cdc1dda95223e9d2d3c58d3b178aa5dacb35ee7e3bbac10e4e1faacb419 \ + --hash=sha256:50c42830a633fa0cf9e7d27664637532791bfc31c731a87b202d2d8ac40c3ea2 \ + --hash=sha256:55f44b440d491028addb3b88f72207d71eeebfb7b5dbf0643f7c023ae1fba619 \ + --hash=sha256:608e7073dfa9e38a85d38474c082d4281f4ce276ac0010224eaba11e929dd53a \ + --hash=sha256:63ba06c9941e46fa389d389644e2d8225e0e3e5ebcc4ff1ea8506dce646f8c8a \ + --hash=sha256:65608c35bfb8a76763f37036547f7adfd09270fbdbf96608be2bead319728fcd \ + --hash=sha256:665a36ae6f8f20a4676b53224e33d456a6f5a72657d9c83c2aa00765072f31f7 \ + --hash=sha256:6d6607f98fcf17e534162f0709aaad3ab7a96032723d8ac8750ffe17ae5a0666 \ + --hash=sha256:7313ce6a199651c4ed9d7e4cfb4aa56fe923b1adf9af3b420ee14e6d9a73df65 \ + --hash=sha256:7668b52e102d0ed87cb082380a7e2e1e78737ddecdde129acadb0eccc5423859 \ + --hash=sha256:7df70907e00c970c60b9ef2938d894a9381f38e6b9db73c5be35e59d92e06625 \ + --hash=sha256:7e007132af78ea9df29495dbf7b5824cb71648d7133cf7848a2a5dd00d36f9ff \ + --hash=sha256:835fb5e38fd89328e9c81067fd642b3593c33e1e17e2fdbf77f5676abb14a156 \ + --hash=sha256:8bca7e26c1dd751236cfb0c6c72d4ad61d986e9a41bbf76cb445f69488b2a2bd \ + --hash=sha256:8db032bf0ce9022a8e41a22598eefc802314e81b879ae093f36ce9ddf39ab1ba \ + --hash=sha256:99625a92da8229df6d44335e6fcc558a5037dd0a760e11d84be2260e6f37002f \ + --hash=sha256:9cad97ab29dfc3f0249b483412c85c8ef4766d96cdf9dcf5a1e3caa3f3661cf1 \ + --hash=sha256:a4abaec6ca3ad8660690236d11bfe28dfd707778e2442b45addd2f086d6ef094 \ + --hash=sha256:a6e40afa7f45939ca356f348c8e23048e02cb109ced1eb8420961b2f40fb373a \ + --hash=sha256:a6f2fcca746e8d5910e18782f976489939d54a91f9411c32051b4aab2bd7c513 \ + --hash=sha256:a806db027852538d2ad7555b203300173dd1b77ba116de92da9afbc3a3be3eed \ + --hash=sha256:abcabc8c2b26036d62d4c746381a6f7cf60aafcc653198ad678306986b09450d \ + --hash=sha256:b8526c6d437855442cdd3d87eede9c425c4445ea011ca38d937db299382e6fa3 \ + --hash=sha256:bb06feb762bade6bf3c8b844462274db0c76acc95c52abe8dbed28ae3d44a147 \ + --hash=sha256:c0a33bc9f02c2b17c3ea382f91b4db0e6cde90b63b296422a939886a7a80de1c \ + --hash=sha256:c4a549890a45f57f1ebf99c067a4ad0cb423a05544accaf2b065246827ed9603 \ + --hash=sha256:ca244fa73f50a800cf8c3ebf7fd93149ec37f5cb9596aa8873ae2c1d23498601 \ + --hash=sha256:cf877ab4ed6e302ec1d04952ca358b381a882fbd9d1b07cccbfd61783561f98a \ + --hash=sha256:d9d971ec1e79906046aa3ca266de79eac42f1dbf3612a05dc9368125952bd1a1 \ + --hash=sha256:da25303d91526aac3672ee6d49a2f3db2d9502a4a60b55519feb1a4c7714e07d \ + --hash=sha256:e55e40ff0cc8cc5c07996915ad367fa47da6b3fc091fdadca7f5403239c5fec3 \ + --hash=sha256:f03a532d7dee1bed20bc4884194a16160a2de9ffc6354b3878ec9682bb623c54 \ + --hash=sha256:f1cd098434e83e656abf198f103a8207a8187c0fc110306691a2e94a78d0abb2 \ + --hash=sha256:f2bfb563d0211ce16b63c7cb9395d2c682a23187f54c3d79bfec33e6705473c6 \ + --hash=sha256:f8ffb705ffcf5ddd0e80b65ddf7bed7ee4f5a441ea7d3419e861a12eaf41af58 + + ----- stderr ----- + Resolved 1 package in [TIME] + "### + ); + + Ok(()) +} + /// `--generate-hashes` should update the hashes in the "lockfile" if the package is upgraded due /// to a change in requirements. #[test] From a01143980ab4073cc31f071c7c013e027483b31b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 11:20:44 -0400 Subject: [PATCH 083/110] Upgrade `reqwest` to v0.12.3 (#2817) ## Summary Closes #2814. --- Cargo.lock | 511 ++++++++----------- Cargo.toml | 10 +- crates/uv-auth/Cargo.toml | 4 +- crates/uv-auth/src/middleware.rs | 2 +- crates/uv-client/Cargo.toml | 10 +- crates/uv-client/src/base_client.rs | 20 +- crates/uv-client/src/lib.rs | 1 - crates/uv-client/src/middleware.rs | 2 +- crates/uv-client/src/tls.rs | 102 ---- crates/uv-client/tests/netrc_auth.rs | 30 +- crates/uv-client/tests/user_agent_version.rs | 68 ++- crates/uv/Cargo.toml | 2 +- 12 files changed, 312 insertions(+), 450 deletions(-) delete mode 100644 crates/uv-client/src/tls.rs diff --git a/Cargo.lock b/Cargo.lock index 5d9293c6a..fb858cf41 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,9 +30,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.1.2" +version = "1.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" dependencies = [ "memchr", ] @@ -123,15 +123,15 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.81" +version = "1.0.82" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519" [[package]] name = "arc-swap" -version = "1.7.0" +version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b3d0060af21e8d11a926981cc00c6c1541aa91dd64b9f881985c3da1094425f" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" [[package]] name = "arrayref" @@ -223,14 +223,14 @@ checksum = "a507401cad91ec6a857ed5513a2073c82a9b9048762b885bb98655b306964681" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] name = "async_http_range_reader" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf8eeab30c68da4dc2c51f3afc4327ab06fe0f3f028ca423f7ca398c7ed8c5e7" +checksum = "8561e6613f8361df8bed11c0eef05b98384643bc81f6b753eec7c1d91f097509" dependencies = [ "bisection", "futures", @@ -262,9 +262,9 @@ dependencies = [ [[package]] name = "autocfg" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" +checksum = "f1fdabc7756949593fe60f30ec81974b613357de856987752631dea1e3394c80" [[package]] name = "axoasset" @@ -307,9 +307,9 @@ dependencies = [ [[package]] name = "axoupdater" -version = "0.4.0" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7306cf5b4c3e7037d27ed5e9f7dc1b24d9941c77601e1e24c7d87839df6d8a2" +checksum = "2245976a147040f18b5b02ecb72b8705efa60f75225e32f51dd717cd5f9d79d1" dependencies = [ "axoasset", "axoprocess", @@ -340,9 +340,9 @@ dependencies = [ [[package]] name = "backtrace" -version = "0.3.69" +version = "0.3.71" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2089b7e3f35b9dd2d0ed921ead4f6d318c27680d4a5bd167b3ee120edb105837" +checksum = "26b05800d2e817c8b3b4b54abd461726265fa9789ae34330622f2db9ee696f9d" dependencies = [ "addr2line", "cc", @@ -403,9 +403,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.4.2" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" +checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1" [[package]] name = "bitvec" @@ -462,9 +462,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.15.4" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ff69b9dd49fd426c69a0db9fc04dd934cdb6645ff000864d98f7e2af8830eaa" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytecheck" @@ -490,9 +490,9 @@ dependencies = [ [[package]] name = "bytemuck" -version = "1.14.3" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2ef034f05691a48569bd920a96c81b9d91bbad1ab5ac7c4616c1f6ef36cb79f" +checksum = "5d6d68c57235a3a081186990eca2867354726650f42f7516ca50c28d6281fd15" [[package]] name = "byteorder" @@ -502,9 +502,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" [[package]] name = "bytes" -version = "1.5.0" +version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +checksum = "514de17de45fdb8dc022b1a7975556c53c86f9f0aa5f534b98977b171857c2c9" [[package]] name = "cache-key" @@ -564,9 +564,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.90" +version = "1.0.92" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5" +checksum = "2678b2e3449475e95b0aa6f9b506a28e61b3dc8996592b983695e8ebb58a8b41" dependencies = [ "jobserver", "libc", @@ -660,9 +660,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.5.1" +version = "4.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "885e4d7d5af40bfb99ae6f9433e292feac98d452dcb3ec3d25dfe7552b77da8c" +checksum = "dd79504325bf38b10165b02e89b4347300f855f273c4cb30c4a3209e6583275e" dependencies = [ "clap", ] @@ -708,7 +708,7 @@ dependencies = [ "heck 0.5.0", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -1065,9 +1065,9 @@ checksum = "a357d28ed41a50f9c765dbfe56cbc04a64e53e5fc58ba79fbc34c10ef3df831f" [[package]] name = "encoding_rs" -version = "0.8.33" +version = "0.8.34" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1" +checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59" dependencies = [ "cfg-if", ] @@ -1099,9 +1099,9 @@ dependencies = [ [[package]] name = "event-listener" -version = "5.2.0" +version = "5.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b5fb89194fa3cad959b833185b3063ba881dbfc7030680b314250779fb4cc91" +checksum = "6d9944b8ca13534cdfb2800775f8dd4902ff3fc75a50101466decadfdf322a24" dependencies = [ "concurrent-queue", "parking", @@ -1110,9 +1110,9 @@ dependencies = [ [[package]] name = "event-listener-strategy" -version = "0.5.0" +version = "0.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "feedafcaa9b749175d5ac357452a9d41ea2911da598fde46ce1fe02c37751291" +checksum = "332f51cb23d20b0de8458b86580878211da09bcd4503cb579c225b3d124cabb3" dependencies = [ "event-listener", "pin-project-lite", @@ -1120,9 +1120,9 @@ dependencies = [ [[package]] name = "fastrand" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +checksum = "658bd65b1cf4c852a3cc96f18a8ce7b5640f6b703f905c7d74532294c2a63984" [[package]] name = "fdeflate" @@ -1283,9 +1283,9 @@ checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" [[package]] name = "futures-lite" -version = "2.2.0" +version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "445ba825b27408685aaecefd65178908c36c6e96aaf6d8599419d46e624192ba" +checksum = "52527eb5074e35e9339c6b4e8d12600c7128b68fb25dcb9fa9dec18f7c25f3a5" dependencies = [ "fastrand", "futures-core", @@ -1302,7 +1302,7 @@ checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -1347,9 +1347,9 @@ dependencies = [ [[package]] name = "getrandom" -version = "0.2.12" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "190092ea657667030ac6a35e305e62fc4dd69fd98ac98631e5d3a2b1575a12b5" +checksum = "94b22e06ecb0110981051723910cbf0b5f5e09a2062dd7663334ee79a9d1286c" dependencies = [ "cfg-if", "js-sys", @@ -1380,7 +1380,7 @@ version = "0.18.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "232e6a7bfe35766bf715e55a88b39a700596c0ccfd88cd3680b4cdb40d66ef70" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", "libgit2-sys", "log", @@ -1405,7 +1405,7 @@ dependencies = [ "bstr", "log", "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -1414,42 +1414,23 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "ignore", "walkdir", ] [[package]] name = "h2" -version = "0.3.24" +version = "0.4.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bb2c4422095b67ee78da96fbb51a4cc413b3b25883c7717ff7ca1ab31022c9c9" +checksum = "816ec7294445779408f36fe57bc5b7fc1cf59664059096c65f905c1c61f58069" dependencies = [ "bytes", "fnv", "futures-core", "futures-sink", "futures-util", - "http 0.2.12", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "h2" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "31d030e59af851932b72ceebadf4a2b5986dba4c3b99dd2493f8273a0f151943" -dependencies = [ - "bytes", - "fnv", - "futures-core", - "futures-sink", - "futures-util", - "http 1.1.0", + "http", "indexmap", "slab", "tokio", @@ -1459,9 +1440,9 @@ dependencies = [ [[package]] name = "half" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5eceaaeec696539ddaf7b333340f1af35a5aa87ae3e4f3ead0532f72affab2e" +checksum = "6dd08c532ae367adf81c312a4580bc67f1d0fe8bc9c460520283f4c0ff277888" dependencies = [ "cfg-if", "crunchy", @@ -1547,17 +1528,6 @@ dependencies = [ "utf8-width", ] -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - [[package]] name = "http" version = "1.1.0" @@ -1569,17 +1539,6 @@ dependencies = [ "itoa", ] -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - [[package]] name = "http-body" version = "1.0.0" @@ -1587,7 +1546,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1cac85db508abc24a2e48553ba12a996e87244a0395ce011e62b37158745d643" dependencies = [ "bytes", - "http 1.1.0", + "http", ] [[package]] @@ -1598,8 +1557,8 @@ checksum = "0475f8b2ac86659c21b64320d5d653f9efe42acd2a4e560073ec61a155a34f1d" dependencies = [ "bytes", "futures-core", - "http 1.1.0", - "http-body 1.0.0", + "http", + "http-body", "pin-project-lite", ] @@ -1621,30 +1580,6 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" -[[package]] -name = "hyper" -version = "0.14.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf96e135eb83a2a8ddf766e426a841d8ddd7449d5f00d34ea02b41d2f19eef80" -dependencies = [ - "bytes", - "futures-channel", - "futures-core", - "futures-util", - "h2 0.3.24", - "http 0.2.12", - "http-body 0.4.6", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", - "want", -] - [[package]] name = "hyper" version = "1.2.0" @@ -1654,9 +1589,9 @@ dependencies = [ "bytes", "futures-channel", "futures-util", - "h2 0.4.2", - "http 1.1.0", - "http-body 1.0.0", + "h2", + "http", + "http-body", "httparse", "httpdate", "itoa", @@ -1668,16 +1603,19 @@ dependencies = [ [[package]] name = "hyper-rustls" -version = "0.24.2" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec3efd23720e2049821a693cbc7e65ea87c72f1c58ff2f9522ff332b1491e590" +checksum = "a0bea761b46ae2b24eb4aef630d8d1c398157b6fc29e6350ecf090a0b70c952c" dependencies = [ "futures-util", - "http 0.2.12", - "hyper 0.14.28", + "http", + "hyper", + "hyper-util", "rustls", + "rustls-pki-types", "tokio", "tokio-rustls", + "tower-service", ] [[package]] @@ -1687,13 +1625,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ca38ef113da30126bbff9cd1705f9273e15d45498615d138b0c20279ac7a76aa" dependencies = [ "bytes", + "futures-channel", "futures-util", - "http 1.1.0", - "http-body 1.0.0", - "hyper 1.2.0", + "http", + "http-body", + "hyper", "pin-project-lite", "socket2", "tokio", + "tower", + "tower-service", + "tracing", ] [[package]] @@ -1790,9 +1732,9 @@ dependencies = [ [[package]] name = "indoc" -version = "2.0.4" +version = "2.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" +checksum = "b248f5224d1d606005e02c97f5aa4e88eeb230488bcc03bc9ca4d7991399f2b5" [[package]] name = "insta" @@ -1896,9 +1838,9 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.10" +version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1a46d1a171d865aa5f83f92695765caa047a9b4cbae2cbf37dbd613a793fd4c" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" [[package]] name = "jobserver" @@ -1990,13 +1932,12 @@ dependencies = [ [[package]] name = "libredox" -version = "0.0.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85c833ca1e66078851dba29046874e38f08b2c883700aa29a03ddd3b23814ee8" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "libc", - "redox_syscall 0.4.1", ] [[package]] @@ -2025,9 +1966,9 @@ dependencies = [ [[package]] name = "libz-sys" -version = "1.1.15" +version = "1.1.16" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "037731f5d3aaa87a5675e895b63ddff1a87624bc29f77004ea829809654e48f6" +checksum = "5e143b5e666b2695d28f6bca6497720813f699c9602dd7f5cac91008b8ada7f9" dependencies = [ "cc", "libc", @@ -2085,9 +2026,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.7.1" +version = "2.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d" [[package]] name = "memmap2" @@ -2118,9 +2059,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.9.0" +version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a" dependencies = [ "autocfg", ] @@ -2153,7 +2094,7 @@ checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -2171,16 +2112,6 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" -[[package]] -name = "mime_guess" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef" -dependencies = [ - "mime", - "unicase", -] - [[package]] name = "miniz_oxide" version = "0.7.2" @@ -2239,7 +2170,7 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "cfg-if", "cfg_aliases", "libc", @@ -2341,9 +2272,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.101" +version = "0.9.102" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dda2b0f344e78efc2facf7d195d098df0dd72151b26ab98da807afc26c198dff" +checksum = "c597637d56fbc83893a35eb0dd04b2b8e7a50c91e64e9493e398b5df4fb45fa2" dependencies = [ "cc", "libc", @@ -2358,6 +2289,16 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" +[[package]] +name = "os_info" +version = "3.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" +dependencies = [ + "log", + "winapi", +] + [[package]] name = "overload" version = "0.1.1" @@ -2512,14 +2453,14 @@ checksum = "2f38a4412a78282e09a2cf38d195ea5420d15ba0602cb375210efbc877243965" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] name = "pin-project-lite" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +checksum = "bda66fc9667c18cb2758a2ac84d1167245054bcf85d5d1aaa6923f45801bdd02" [[package]] name = "pin-utils" @@ -2678,7 +2619,7 @@ dependencies = [ "cfg-if", "indoc", "libc", - "memoffset 0.9.0", + "memoffset 0.9.1", "parking_lot 0.12.1", "portable-atomic", "pyo3-build-config", @@ -2727,7 +2668,7 @@ dependencies = [ "proc-macro2", "pyo3-macros-backend", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -2740,7 +2681,7 @@ dependencies = [ "proc-macro2", "pyo3-build-config", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -2765,9 +2706,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.35" +version = "1.0.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "291ec9ab5efd934aaf503a6466c5d5251535d108ee747472c3977cc5acc868ef" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" dependencies = [ "proc-macro2", ] @@ -2869,9 +2810,9 @@ dependencies = [ [[package]] name = "redox_users" -version = "0.4.4" +version = "0.4.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a18479200779601e498ada4e8c1e1f50e3ee19deb0259c25825a98b5603b2cb4" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" dependencies = [ "getrandom", "libredox", @@ -2898,7 +2839,7 @@ dependencies = [ "aho-corasick", "memchr", "regex-automata 0.4.6", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -2918,7 +2859,7 @@ checksum = "86b83b8b9847f9bf95ef68afb0b8e6cdb80f498442f5179a29fad448fcc1eaea" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.8.2", + "regex-syntax 0.8.3", ] [[package]] @@ -2929,9 +2870,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.8.2" +version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" +checksum = "adad44e29e4c806119491a7f06f03de4d1af22c3a680dd47f1e6e179439d1f56" [[package]] name = "rend" @@ -2972,37 +2913,37 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.11.26" +version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78bf93c4af7a8bb7d879d51cebe797356ff10ae8516ace542b5182d9dcac10b2" +checksum = "3e6cc1e89e689536eb5aeede61520e874df5a4707df811cd5da4aa5fbb2aae19" dependencies = [ "async-compression", - "base64 0.21.7", + "base64 0.22.0", "bytes", - "encoding_rs", + "futures-channel", "futures-core", "futures-util", - "h2 0.3.24", - "http 0.2.12", - "http-body 0.4.6", - "hyper 0.14.28", + "http", + "http-body", + "http-body-util", + "hyper", "hyper-rustls", + "hyper-util", "ipnet", "js-sys", "log", "mime", - "mime_guess", "once_cell", "percent-encoding", "pin-project-lite", "rustls", "rustls-native-certs", "rustls-pemfile", + "rustls-pki-types", "serde", "serde_json", "serde_urlencoded", "sync_wrapper", - "system-configuration", "tokio", "tokio-rustls", "tokio-util", @@ -3018,37 +2959,36 @@ dependencies = [ [[package]] name = "reqwest-middleware" -version = "0.2.5" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a735987236a8e238bf0296c7e351b999c188ccc11477f311b82b55c93984216" +checksum = "0209efb52486ad88136190094ee214759ef7507068b27992256ed6610eb71a01" dependencies = [ "anyhow", "async-trait", - "http 0.2.12", + "http", "reqwest", "serde", - "task-local-extensions", "thiserror", + "tower-service", ] [[package]] name = "reqwest-retry" -version = "0.3.0" +version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9af20b65c2ee9746cc575acb6bd28a05ffc0d15e25c992a8f4462d8686aacb4f" +checksum = "40f342894422862af74c50e1e9601cf0931accc9c6981e5eb413c46603b616b5" dependencies = [ "anyhow", "async-trait", "chrono", "futures", "getrandom", - "http 0.2.12", - "hyper 0.14.28", + "http", + "hyper", "parking_lot 0.11.2", "reqwest", "reqwest-middleware", "retry-policies", - "task-local-extensions", "tokio", "tracing", "wasm-timer", @@ -3075,9 +3015,9 @@ dependencies = [ [[package]] name = "retry-policies" -version = "0.2.1" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17dd00bff1d737c40dbcd47d4375281bf4c17933f9eef0a185fc7bacca23ecbd" +checksum = "493b4243e32d6eedd29f9a398896e35c6943a123b55eec97dcaee98310d25810" dependencies = [ "anyhow", "chrono", @@ -3210,11 +3150,11 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.38.31" +version = "0.38.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ea3e1a662af26cd7a3ba09c0297a31af215563ecf42817c98df621387f4e949" +checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89" dependencies = [ - "bitflags 2.4.2", + "bitflags 2.5.0", "errno", "libc", "linux-raw-sys", @@ -3223,44 +3163,55 @@ dependencies = [ [[package]] name = "rustls" -version = "0.21.10" +version = "0.22.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9d5a6813c0759e4609cd494e8e725babae6a2ca7b62a5536a13daaec6fcb7ba" +checksum = "99008d7ad0bbbea527ec27bddbc0e432c5b87d8175178cee68d2eec9c4a1813c" dependencies = [ "log", "ring", + "rustls-pki-types", "rustls-webpki", - "sct", + "subtle", + "zeroize", ] [[package]] name = "rustls-native-certs" -version = "0.6.3" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9aace74cb666635c918e9c12bc0d348266037aa8eb599b5cba565709a8dff00" +checksum = "8f1fb85efa936c42c6d5fc28d2629bb51e4b2f4b8a5211e297d599cc5a093792" dependencies = [ "openssl-probe", "rustls-pemfile", + "rustls-pki-types", "schannel", "security-framework", ] [[package]] name = "rustls-pemfile" -version = "1.0.4" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c74cae0a4cf6ccbbf5f359f08efdf8ee7e1dc532573bf0db71968cb56b1448c" +checksum = "29993a25686778eb88d4189742cd713c9bce943bc54251a33509dc63cbacf73d" dependencies = [ - "base64 0.21.7", + "base64 0.22.0", + "rustls-pki-types", ] [[package]] -name = "rustls-webpki" -version = "0.101.7" +name = "rustls-pki-types" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +checksum = "ecd36cc4259e3e4514335c4a138c6b43171a8d61d8f5c9348f9fc7529416f247" + +[[package]] +name = "rustls-webpki" +version = "0.102.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "faaa0a62740bedb9b2ef5afa303da42764c012f743917351dc9a237ea1663610" dependencies = [ "ring", + "rustls-pki-types", "untrusted", ] @@ -3310,16 +3261,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" -[[package]] -name = "sct" -version = "0.7.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" -dependencies = [ - "ring", - "untrusted", -] - [[package]] name = "seahash" version = "4.1.0" @@ -3328,9 +3269,9 @@ checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" [[package]] name = "security-framework" -version = "2.9.2" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05b64fb303737d99b81884b2c63433e9ae28abebe5eb5045dcdd175dc2ecf4de" +checksum = "770452e37cad93e0a50d5abc3990d2bc351c36d0328f86cefec2f2fb206eaef6" dependencies = [ "bitflags 1.3.2", "core-foundation", @@ -3341,9 +3282,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.9.1" +version = "2.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e932934257d3b408ed8f30db49d85ea163bfe74961f017f405b025af298f0c7a" +checksum = "41f3cc463c0ef97e11c3461a9d3787412d30e8e7eb907c79180c4a57bf7c04ef" dependencies = [ "core-foundation-sys", "libc", @@ -3372,7 +3313,7 @@ checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -3467,9 +3408,9 @@ checksum = "f27f6278552951f1f2b8cf9da965d10969b2efdea95a6ec47987ab46edfe263a" [[package]] name = "similar" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32fea41aca09ee824cc9724996433064c89f7777e60762749a4170a14abbfa21" +checksum = "fa42c91313f1d05da9b26f267f931cf178d4aba455b4c4622dd7355eb80c6640" [[package]] name = "simplecss" @@ -3497,9 +3438,9 @@ dependencies = [ [[package]] name = "smallvec" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6ecd384b10a64542d77071bd64bd7b231f4ed5940fba55e98c3de13824cf3d7" +checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" [[package]] name = "smawk" @@ -3534,9 +3475,9 @@ dependencies = [ [[package]] name = "strsim" -version = "0.11.0" +version = "0.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ee073c9e4cd00e28217186dbe12796d692868f432bf2e97ee73bed0c56dfa01" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "subtle" @@ -3614,9 +3555,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.52" +version = "2.0.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b699d15b36d1f02c3e7c69f8ffef53de37aefae075d8488d4ba1a7788d574a07" +checksum = "44cfb93f38070beee36b3fef7d4f5a16f27751d94b187b666a5cc5e9b0d30687" dependencies = [ "proc-macro2", "quote", @@ -3639,27 +3580,6 @@ dependencies = [ "libc", ] -[[package]] -name = "system-configuration" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" -dependencies = [ - "bitflags 1.3.2", - "core-foundation", - "system-configuration-sys", -] - -[[package]] -name = "system-configuration-sys" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a75fb188eb626b924683e3b95e3a48e63551fcfb51949de2f06a9d91dbee93c9" -dependencies = [ - "core-foundation-sys", - "libc", -] - [[package]] name = "tagu" version = "0.1.6" @@ -3739,7 +3659,7 @@ dependencies = [ "cfg-if", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -3750,7 +3670,7 @@ checksum = "5c89e72a01ed4c579669add59014b9a524d609c0c88c6a585ce37485879f6ffb" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", "test-case-core", ] @@ -3791,7 +3711,7 @@ checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -3907,16 +3827,17 @@ checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] name = "tokio-rustls" -version = "0.24.1" +version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c28327cf380ac148141087fbfb9de9d7bd4e84ab5d2c28fbc911d753de8a7081" +checksum = "775e0c0f0adb3a2f22a00c4745d728b479985fc15ee7ca6a2608388c5569860f" dependencies = [ "rustls", + "rustls-pki-types", "tokio", ] @@ -3985,9 +3906,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.22.8" +version = "0.22.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd" +checksum = "8e40bb779c5187258fd7aad0eb68cb8706a0a81fa712fbea808ab43c4b8374c4" dependencies = [ "indexmap", "serde", @@ -3996,6 +3917,28 @@ dependencies = [ "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-layer" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c20c8dbed6283a09604c3e69b4b7eeb54e298b8a600d4d5ecb5ad39de609f1d0" + [[package]] name = "tower-service" version = "0.3.2" @@ -4022,7 +3965,7 @@ checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -4137,15 +4080,6 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" -[[package]] -name = "unicase" -version = "2.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f7d2d4dafb69621809a81864c9c1b864479e1235c0dd4e199924b9742439ed89" -dependencies = [ - "version_check", -] - [[package]] name = "unicode-bidi" version = "0.3.15" @@ -4367,11 +4301,11 @@ dependencies = [ "async-trait", "base64 0.22.0", "clap", + "http", "once_cell", "reqwest", "reqwest-middleware", "rust-netrc", - "task-local-extensions", "tempfile", "thiserror", "tokio", @@ -4448,10 +4382,13 @@ dependencies = [ "fs-err", "futures", "html-escape", - "http 0.2.12", - "hyper 0.14.28", + "http", + "http-body-util", + "hyper", + "hyper-util", "insta", "install-wheel-rs", + "os_info", "pep440_rs", "pep508_rs", "platform-tags", @@ -4462,8 +4399,6 @@ dependencies = [ "rkyv", "rmp-serde", "rustc-hash", - "rustls", - "rustls-native-certs", "serde", "serde_json", "sys-info", @@ -4483,7 +4418,6 @@ dependencies = [ "uv-normalize", "uv-version", "uv-warnings", - "webpki-roots", ] [[package]] @@ -4979,7 +4913,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", "wasm-bindgen-shared", ] @@ -5013,7 +4947,7 @@ checksum = "e94f17b526d0a461a191c78ea52bbce64071ed5c04c9ffe424dcb38f74171bb7" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -5064,9 +4998,12 @@ dependencies = [ [[package]] name = "webpki-roots" -version = "0.25.4" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +checksum = "b3de34ae270483955a94f4b21bdaaeb83d508bb84a01435f393818edb0012009" +dependencies = [ + "rustls-pki-types", +] [[package]] name = "weezl" @@ -5088,9 +5025,9 @@ dependencies = [ [[package]] name = "widestring" -version = "1.0.2" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "653f141f39ec16bba3c5abe400a0c60da7468261cc2cbf36805022876bc721a8" +checksum = "7219d36b6eac893fa81e84ebe06485e7dcbb616177469b142df14f1f4deb1311" [[package]] name = "winapi" @@ -5172,7 +5109,7 @@ checksum = "12168c33176773b86799be25e2a2ba07c7aab9968b37541f1094dbd7a60c8946" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -5183,7 +5120,7 @@ checksum = "9d8dc32e0095a7eeccebd0e3f09e9509365ecb3fc6ac4d6f5f14a3f6392942d1" dependencies = [ "proc-macro2", "quote", - "syn 2.0.52", + "syn 2.0.58", ] [[package]] @@ -5338,9 +5275,9 @@ dependencies = [ [[package]] name = "winreg" -version = "0.50.0" +version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "524e57b2c537c0f9b1e69f1965311ec12182b4122e45035b1508cd24d2adadb1" +checksum = "a277a57398d4bfa075df44f501a17cfdf8542d224f0d36095a2adc7aee4ef0a5" dependencies = [ "cfg-if", "windows-sys 0.48.0", @@ -5363,9 +5300,9 @@ dependencies = [ "base64 0.21.7", "deadpool", "futures", - "http 1.1.0", + "http", "http-body-util", - "hyper 1.2.0", + "hyper", "hyper-util", "log", "once_cell", @@ -5416,6 +5353,12 @@ version = "0.13.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "66fee0b777b0f5ac1c69bb06d361268faafa61cd4682ae064a171c16c433e9e4" +[[package]] +name = "zeroize" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "525b4ec142c6b68a2d10f01f7bbf6755599ca3f81ea53b8431b7dd348f5fdb2d" + [[package]] name = "zip" version = "0.6.6" diff --git a/Cargo.toml b/Cargo.toml index 52f2123a5..2826ddc7a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -55,7 +55,7 @@ anyhow = { version = "1.0.80" } async-channel = { version = "2.2.0" } async-compression = { version = "0.4.6" } async-trait = { version = "0.1.78" } -async_http_range_reader = { version = "0.7.0" } +async_http_range_reader = { version = "0.7.1" } async_zip = { git = "https://github.com/charliermarsh/rs-async-zip", rev = "1dcb40cfe1bf5325a6fd4bfcf9894db40241f585", features = ["deflate"] } axoupdater = { version = "0.4.0", default-features = false } backoff = { version = "0.4.0" } @@ -86,7 +86,7 @@ hex = { version = "0.4.3" } hmac = { version = "0.12.1" } home = { version = "0.5.9" } html-escape = { version = "0.2.13" } -http = { version = "0.2.12" } +http = { version = "1.1.0" } indexmap = { version = "2.2.5" } indicatif = { version = "0.17.7" } indoc = { version = "2.0.4" } @@ -107,9 +107,9 @@ rand = { version = "0.8.5" } rayon = { version = "1.8.0" } reflink-copy = { version = "0.1.15" } regex = { version = "1.10.2" } -reqwest = { version = "0.11.23", default-features = false, features = ["json", "gzip", "brotli", "stream", "rustls-tls", "rustls-tls-native-roots"] } -reqwest-middleware = { version = "0.2.4" } -reqwest-retry = { version = "0.3.0" } +reqwest = { version = "0.12.3", default-features = false, features = ["json", "gzip", "brotli", "stream", "rustls-tls", "rustls-tls-native-roots"] } +reqwest-middleware = { version = "0.3.0" } +reqwest-retry = { version = "0.5.0" } rkyv = { version = "0.7.43", features = ["strict", "validation"] } rmp-serde = { version = "1.1.2" } rust-netrc = { version = "0.1.1" } diff --git a/crates/uv-auth/Cargo.toml b/crates/uv-auth/Cargo.toml index 0e4851fe1..87677f03d 100644 --- a/crates/uv-auth/Cargo.toml +++ b/crates/uv-auth/Cargo.toml @@ -7,15 +7,15 @@ edition = "2021" async-trait = { workspace = true } base64 = { workspace = true } clap = { workspace = true, features = ["derive", "env"], optional = true } +http = { workspace = true } +once_cell = { workspace = true } reqwest = { workspace = true } reqwest-middleware = { workspace = true } rust-netrc = { workspace = true } -task-local-extensions = { workspace = true } thiserror = { workspace = true } tracing = { workspace = true } url = { workspace = true } urlencoding = { workspace = true } -once_cell = { workspace = true } [dev-dependencies] tempfile = { workspace = true } diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index d21ca7847..5d5e14c4a 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -1,9 +1,9 @@ +use http::Extensions; use std::path::Path; use netrc::Netrc; use reqwest::{header::HeaderValue, Request, Response}; use reqwest_middleware::{Middleware, Next}; -use task_local_extensions::Extensions; use tracing::{debug, warn}; use crate::{ diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index bddd71163..10d0cee28 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -48,13 +48,11 @@ tracing = { workspace = true } url = { workspace = true } urlencoding = { workspace = true } -# These must be kept in-sync with those used by `reqwest`. -rustls = { version = "0.21.10" } -rustls-native-certs = { version = "0.6.3" } -webpki-roots = { version = "0.25.4" } - [dev-dependencies] anyhow = { workspace = true } -hyper = { version = "0.14.28", features = ["server", "http1"] } +http-body-util = { version = "0.1.0" } +hyper = { version = "1.2.0", features = ["server", "http1"] } +hyper-util = { version = "0.1.3", features = ["tokio"] } insta = { version = "1.36.1" } +os_info = { version = "=3.7.0", default-features = false } tokio = { workspace = true, features = ["fs", "macros"] } diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index c45974d48..fffcf0848 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -16,8 +16,7 @@ use uv_warnings::warn_user_once; use crate::linehaul::LineHaul; use crate::middleware::OfflineMiddleware; -use crate::tls::Roots; -use crate::{tls, Connectivity}; +use crate::Connectivity; /// A builder for an [`BaseClient`]. #[derive(Debug, Clone)] @@ -140,19 +139,20 @@ impl<'a> BaseClientBuilder<'a> { } path_exists }); - // Load the TLS configuration. - let tls = tls::load(if self.native_tls || ssl_cert_file_exists { - Roots::Native - } else { - Roots::Webpki - }) - .expect("Failed to load TLS configuration."); + // Configure the builder. let client_core = ClientBuilder::new() .user_agent(user_agent_string) .pool_max_idle_per_host(20) .timeout(std::time::Duration::from_secs(timeout)) - .use_preconfigured_tls(tls); + .tls_built_in_root_certs(false); + + // Configure TLS. + let client_core = if self.native_tls || ssl_cert_file_exists { + client_core.tls_built_in_native_certs(true) + } else { + client_core.tls_built_in_webpki_certs(true) + }; client_core.build().expect("Failed to build HTTP client.") }); diff --git a/crates/uv-client/src/lib.rs b/crates/uv-client/src/lib.rs index 62fef2313..1e2adc731 100644 --- a/crates/uv-client/src/lib.rs +++ b/crates/uv-client/src/lib.rs @@ -20,4 +20,3 @@ mod middleware; mod registry_client; mod remote_metadata; mod rkyvutil; -mod tls; diff --git a/crates/uv-client/src/middleware.rs b/crates/uv-client/src/middleware.rs index e83d45547..37b991b3b 100644 --- a/crates/uv-client/src/middleware.rs +++ b/crates/uv-client/src/middleware.rs @@ -1,8 +1,8 @@ +use http::Extensions; use std::fmt::Debug; use reqwest::{Request, Response}; use reqwest_middleware::{Middleware, Next}; -use task_local_extensions::Extensions; use url::Url; /// A custom error type for the offline middleware. diff --git a/crates/uv-client/src/tls.rs b/crates/uv-client/src/tls.rs deleted file mode 100644 index 7118a4c05..000000000 --- a/crates/uv-client/src/tls.rs +++ /dev/null @@ -1,102 +0,0 @@ -use rustls::ClientConfig; -use tracing::warn; - -#[derive(thiserror::Error, Debug)] -pub(crate) enum TlsError { - #[error(transparent)] - Rustls(#[from] rustls::Error), - #[error("zero valid certificates found in native root store")] - ZeroCertificates, - #[error("failed to load native root certificates")] - NativeCertificates(#[source] std::io::Error), -} - -#[derive(Debug, Clone, Copy)] -pub(crate) enum Roots { - /// Use reqwest's `rustls-tls-webpki-roots` behavior for loading root certificates. - Webpki, - /// Use reqwest's `rustls-tls-native-roots` behavior for loading root certificates. - Native, -} - -/// Initialize a TLS configuration for the client. -/// -/// This is equivalent to the TLS initialization `reqwest` when `rustls-tls` is enabled, -/// with two notable changes: -/// -/// 1. It enables _either_ the `webpki-roots` or the `native-certs` feature, but not both. -/// 2. It assumes the following builder settings (which match the defaults): -/// - `root_certs: vec![]` -/// - `min_tls_version: None` -/// - `max_tls_version: None` -/// - `identity: None` -/// - `certs_verification: false` -/// - `tls_sni: true` -/// - `http_version_pref: HttpVersionPref::All` -/// -/// See: -pub(crate) fn load(roots: Roots) -> Result { - // Set root certificates. - let mut root_cert_store = rustls::RootCertStore::empty(); - - match roots { - Roots::Webpki => { - // Use `rustls-tls-webpki-roots` - use rustls::OwnedTrustAnchor; - - let trust_anchors = webpki_roots::TLS_SERVER_ROOTS.iter().map(|trust_anchor| { - OwnedTrustAnchor::from_subject_spki_name_constraints( - trust_anchor.subject, - trust_anchor.spki, - trust_anchor.name_constraints, - ) - }); - - root_cert_store.add_trust_anchors(trust_anchors); - } - Roots::Native => { - // Use: `rustls-tls-native-roots` - let mut valid_count = 0; - let mut invalid_count = 0; - for cert in - rustls_native_certs::load_native_certs().map_err(TlsError::NativeCertificates)? - { - let cert = rustls::Certificate(cert.0); - // Continue on parsing errors, as native stores often include ancient or syntactically - // invalid certificates, like root certificates without any X509 extensions. - // Inspiration: https://github.com/rustls/rustls/blob/633bf4ba9d9521a95f68766d04c22e2b01e68318/rustls/src/anchors.rs#L105-L112 - match root_cert_store.add(&cert) { - Ok(_) => valid_count += 1, - Err(err) => { - invalid_count += 1; - warn!( - "rustls failed to parse DER certificate {:?} {:?}", - &err, &cert - ); - } - } - } - if valid_count == 0 && invalid_count > 0 { - return Err(TlsError::ZeroCertificates); - } - } - } - - // Build TLS config - let config_builder = ClientConfig::builder() - .with_safe_default_cipher_suites() - .with_safe_default_kx_groups() - .with_protocol_versions(rustls::ALL_VERSIONS)? - .with_root_certificates(root_cert_store); - - // Finalize TLS config - let mut tls = config_builder.with_no_client_auth(); - - // Enable SNI - tls.enable_sni = true; - - // ALPN protocol - tls.alpn_protocols = vec!["h2".into(), "http/1.1".into()]; - - Ok(tls) -} diff --git a/crates/uv-client/tests/netrc_auth.rs b/crates/uv-client/tests/netrc_auth.rs index 4678f176b..8d657169f 100644 --- a/crates/uv-client/tests/netrc_auth.rs +++ b/crates/uv-client/tests/netrc_auth.rs @@ -3,10 +3,13 @@ use std::io::Write; use anyhow::Result; use futures::future; -use hyper::header::AUTHORIZATION; -use hyper::server::conn::Http; +use http::header::AUTHORIZATION; +use http_body_util::Full; +use hyper::body::Bytes; +use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper::{Body, Request, Response}; +use hyper::{Request, Response}; +use hyper_util::rt::TokioIo; use tempfile::NamedTempFile; use tokio::net::TcpListener; @@ -21,7 +24,7 @@ async fn test_client_with_netrc_credentials() -> Result<()> { // Spawn the server loop in a background task tokio::spawn(async move { - let svc = service_fn(move |req: Request| { + let svc = service_fn(move |req: Request| { // Get User Agent Header and send it back in the response let auth = req .headers() @@ -29,16 +32,19 @@ async fn test_client_with_netrc_credentials() -> Result<()> { .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()) .unwrap_or_default(); // Empty Default - future::ok::<_, hyper::Error>(Response::new(Body::from(auth))) + future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(auth)))) }); - // Start Hyper Server + // Start Server (not wrapped in loop {} since we want a single response server) + // If you want server to accept multiple connections, wrap it in loop {} let (socket, _) = listener.accept().await.unwrap(); - Http::new() - .http1_keep_alive(false) - .serve_connection(socket, svc) - .with_upgrades() - .await - .expect("Server Started"); + let socket = TokioIo::new(socket); + tokio::task::spawn(async move { + http1::Builder::new() + .serve_connection(socket, svc) + .with_upgrades() + .await + .expect("Server Started"); + }); }); // Create a netrc file diff --git a/crates/uv-client/tests/user_agent_version.rs b/crates/uv-client/tests/user_agent_version.rs index c1e59c66e..383e0b758 100644 --- a/crates/uv-client/tests/user_agent_version.rs +++ b/crates/uv-client/tests/user_agent_version.rs @@ -1,12 +1,16 @@ use anyhow::Result; use futures::future; +use http_body_util::Full; +use hyper::body::Bytes; use hyper::header::USER_AGENT; -use hyper::server::conn::Http; +use hyper::server::conn::http1; use hyper::service::service_fn; -use hyper::{Body, Request, Response}; +use hyper::{Request, Response}; +use hyper_util::rt::TokioIo; +use tokio::net::TcpListener; + use pep508_rs::{MarkerEnvironment, StringVersion}; use platform_tags::{Arch, Os, Platform}; -use tokio::net::TcpListener; use uv_cache::Cache; use uv_client::LineHaul; use uv_client::RegistryClientBuilder; @@ -19,8 +23,8 @@ async fn test_user_agent_has_version() -> Result<()> { let addr = listener.local_addr()?; // Spawn the server loop in a background task - tokio::spawn(async move { - let svc = service_fn(move |req: Request| { + let server_task = tokio::spawn(async move { + let svc = service_fn(move |req: Request| { // Get User Agent Header and send it back in the response let user_agent = req .headers() @@ -28,16 +32,19 @@ async fn test_user_agent_has_version() -> Result<()> { .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()) .unwrap_or_default(); // Empty Default - future::ok::<_, hyper::Error>(Response::new(Body::from(user_agent))) + future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(user_agent)))) }); - // Start Hyper Server + // Start Server (not wrapped in loop {} since we want a single response server) + // If you want server to accept multiple connections, wrap it in loop {} let (socket, _) = listener.accept().await.unwrap(); - Http::new() - .http1_keep_alive(false) - .serve_connection(socket, svc) - .with_upgrades() - .await - .expect("Server Started"); + let socket = TokioIo::new(socket); + tokio::task::spawn(async move { + http1::Builder::new() + .serve_connection(socket, svc) + .with_upgrades() + .await + .expect("Server Started"); + }); }); // Initialize uv-client @@ -46,7 +53,8 @@ async fn test_user_agent_has_version() -> Result<()> { // Send request to our dummy server let res = client - .uncached_client() + .cached_client() + .uncached() .get(format!("http://{addr}")) .send() .await?; @@ -60,6 +68,9 @@ async fn test_user_agent_has_version() -> Result<()> { // Verify body matches regex assert_eq!(body, format!("uv/{}", version())); + // Wait for the server task to complete, to be a good citizen. + server_task.await?; + Ok(()) } @@ -70,8 +81,8 @@ async fn test_user_agent_has_linehaul() -> Result<()> { let addr = listener.local_addr()?; // Spawn the server loop in a background task - tokio::spawn(async move { - let svc = service_fn(move |req: Request| { + let server_task = tokio::spawn(async move { + let svc = service_fn(move |req: Request| { // Get User Agent Header and send it back in the response let user_agent = req .headers() @@ -79,16 +90,19 @@ async fn test_user_agent_has_linehaul() -> Result<()> { .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()) .unwrap_or_default(); // Empty Default - future::ok::<_, hyper::Error>(Response::new(Body::from(user_agent))) + future::ok::<_, hyper::Error>(Response::new(Full::new(Bytes::from(user_agent)))) }); - // Start Hyper Server + // Start Server (not wrapped in loop {} since we want a single response server) + // If you want server to accept multiple connections, wrap it in loop {} let (socket, _) = listener.accept().await.unwrap(); - Http::new() - .http1_keep_alive(false) - .serve_connection(socket, svc) - .with_upgrades() - .await - .expect("Server Started"); + let socket = TokioIo::new(socket); + tokio::task::spawn(async move { + http1::Builder::new() + .serve_connection(socket, svc) + .with_upgrades() + .await + .expect("Server Started"); + }); }); // Add some representative markers for an Ubuntu CI runner @@ -142,7 +156,8 @@ async fn test_user_agent_has_linehaul() -> Result<()> { // Send request to our dummy server let res = client - .uncached_client() + .cached_client() + .uncached() .get(format!("http://{addr}")) .send() .await?; @@ -153,6 +168,9 @@ async fn test_user_agent_has_linehaul() -> Result<()> { // Check User Agent let body = res.text().await?; + // Wait for the server task to complete, to be a good citizen. + server_task.await?; + // Unpack User-Agent with linehaul let (uv_version, uv_linehaul) = body .split_once(' ') diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index efe192180..35db81fef 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -79,7 +79,7 @@ indoc = { version = "2.0.4" } insta = { version = "1.36.1", features = ["filters", "json"] } predicates = { version = "3.0.4" } regex = { version = "1.10.3" } -reqwest = { version = "0.11.23", features = ["blocking"], default-features = false } +reqwest = { workspace = true, features = ["blocking"], default-features = false } [features] default = ["flate2/zlib-ng", "python", "pypi", "git", "maturin", "python-patch"] From 7cd98d24995122ffe58c41f05e7de28cf9747d60 Mon Sep 17 00:00:00 2001 From: Chan Kang Date: Wed, 10 Apr 2024 12:05:58 -0400 Subject: [PATCH 084/110] Implement `--emit-index-annotation` to annotate source index for each package (#2926) ## Summary resolves https://github.com/astral-sh/uv/issues/2852 ## Test Plan add a couple of tests: - one covering the simplest case with all packages pulled from a single index. - another where packages are pull from two distinct indices. tested manually as well: ``` $ (echo 'pandas'; echo 'torch') | UV_EXTRA_INDEX_URL='https://download.pytorch.org/whl/cpu' cargo run pip compile - --include-indices Finished dev [unoptimized + debuginfo] target(s) in 0.60s Running `target/debug/uv pip compile - --include-indices` Resolved 15 packages in 686ms # This file was autogenerated by uv via the following command: # uv pip compile - --include-indices filelock==3.9.0 # via torch # from https://download.pytorch.org/whl/cpu fsspec==2023.4.0 # via torch # from https://download.pytorch.org/whl/cpu jinja2==3.1.2 # via torch # from https://download.pytorch.org/whl/cpu markupsafe==2.1.3 # via jinja2 # from https://download.pytorch.org/whl/cpu mpmath==1.3.0 # via sympy # from https://download.pytorch.org/whl/cpu networkx==3.2.1 # via torch # from https://download.pytorch.org/whl/cpu numpy==1.26.3 # via pandas # from https://download.pytorch.org/whl/cpu pandas==2.2.1 # from https://pypi.org/simple python-dateutil==2.9.0.post0 # via pandas # from https://pypi.org/simple pytz==2024.1 # via pandas # from https://pypi.org/simple six==1.16.0 # via python-dateutil # from https://pypi.org/simple sympy==1.12 # via torch # from https://download.pytorch.org/whl/cpu torch==2.2.2 # from https://download.pytorch.org/whl/cpu typing-extensions==4.8.0 # via torch # from https://download.pytorch.org/whl/cpu tzdata==2024.1 # via pandas # from https://pypi.org/simple ``` --- crates/distribution-types/src/lib.rs | 27 +++- crates/distribution-types/src/resolved.rs | 12 +- crates/uv-resolver/src/resolution.rs | 27 +++- crates/uv/src/commands/pip_compile.rs | 2 + crates/uv/src/main.rs | 12 +- crates/uv/tests/pip_compile.rs | 146 ++++++++++++++++++++++ 6 files changed, 219 insertions(+), 7 deletions(-) diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index 4b84e6e38..6c2ad6c81 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -371,6 +371,14 @@ impl Dist { } } + /// Returns the [`IndexUrl`], if the distribution is from a registry. + pub fn index(&self) -> Option<&IndexUrl> { + match self { + Self::Built(dist) => dist.index(), + Self::Source(dist) => dist.index(), + } + } + /// Returns the [`File`] instance, if this dist is from a registry with simple json api support pub fn file(&self) -> Option<&File> { match self { @@ -388,7 +396,16 @@ impl Dist { } impl BuiltDist { - /// Returns the [`File`] instance, if this dist is from a registry with simple json api support + /// Returns the [`IndexUrl`], if the distribution is from a registry. + pub fn index(&self) -> Option<&IndexUrl> { + match self { + Self::Registry(registry) => Some(®istry.index), + Self::DirectUrl(_) => None, + Self::Path(_) => None, + } + } + + /// Returns the [`File`] instance, if this distribution is from a registry. pub fn file(&self) -> Option<&File> { match self { Self::Registry(registry) => Some(®istry.file), @@ -406,6 +423,14 @@ impl BuiltDist { } impl SourceDist { + /// Returns the [`IndexUrl`], if the distribution is from a registry. + pub fn index(&self) -> Option<&IndexUrl> { + match self { + Self::Registry(registry) => Some(®istry.index), + Self::DirectUrl(_) | Self::Git(_) | Self::Path(_) => None, + } + } + /// Returns the [`File`] instance, if this dist is from a registry with simple json api support pub fn file(&self) -> Option<&File> { match self { diff --git a/crates/distribution-types/src/resolved.rs b/crates/distribution-types/src/resolved.rs index 91015e4f3..1f72ba4a0 100644 --- a/crates/distribution-types/src/resolved.rs +++ b/crates/distribution-types/src/resolved.rs @@ -3,8 +3,8 @@ use std::fmt::{Display, Formatter}; use pep508_rs::PackageName; use crate::{ - Dist, DistributionId, DistributionMetadata, Identifier, InstalledDist, Name, ResourceId, - VersionOrUrl, + Dist, DistributionId, DistributionMetadata, Identifier, IndexUrl, InstalledDist, Name, + ResourceId, VersionOrUrl, }; /// A distribution that can be used for resolution and installation. @@ -31,6 +31,14 @@ impl ResolvedDist { Self::Installed(dist) => dist.is_editable(), } } + + /// Returns the [`IndexUrl`], if the distribution is from a registry. + pub fn index(&self) -> Option<&IndexUrl> { + match self { + Self::Installable(dist) => dist.index(), + Self::Installed(_) => None, + } + } } impl ResolvedDistRef<'_> { diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index fe81b49b3..9dbf758dc 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -12,7 +12,7 @@ use pubgrub::type_aliases::SelectedDependencies; use rustc_hash::{FxHashMap, FxHashSet}; use distribution_types::{ - Dist, DistributionMetadata, LocalEditable, Name, PackageId, ResolvedDist, Verbatim, + Dist, DistributionMetadata, IndexUrl, LocalEditable, Name, PackageId, ResolvedDist, Verbatim, VersionOrUrl, }; use once_map::OnceMap; @@ -499,6 +499,7 @@ impl ResolutionGraph { /// 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 ResolutionGraph, @@ -511,6 +512,8 @@ pub struct DisplayResolutionGraph<'a> { /// Whether to include annotations in the output, to indicate which dependency or dependencies /// requested each package. include_annotations: bool, + /// Whether to include indexes in the output, to indicate which index was used for each package. + include_index_annotation: bool, /// The style of annotation comments, used to indicate the dependencies that requested each /// package. annotation_style: AnnotationStyle, @@ -524,6 +527,7 @@ impl<'a> From<&'a ResolutionGraph> for DisplayResolutionGraph<'a> { false, false, true, + false, AnnotationStyle::default(), ) } @@ -531,12 +535,14 @@ impl<'a> From<&'a ResolutionGraph> for DisplayResolutionGraph<'a> { impl<'a> DisplayResolutionGraph<'a> { /// Create a new [`DisplayResolutionGraph`] for the given graph. + #[allow(clippy::fn_params_excessive_bools)] pub fn new( underlying: &'a ResolutionGraph, no_emit_packages: &'a [PackageName], show_hashes: bool, include_extras: bool, include_annotations: bool, + include_index_annotation: bool, annotation_style: AnnotationStyle, ) -> DisplayResolutionGraph<'a> { Self { @@ -545,6 +551,7 @@ impl<'a> DisplayResolutionGraph<'a> { show_hashes, include_extras, include_annotations, + include_index_annotation, annotation_style, } } @@ -582,6 +589,14 @@ impl<'a> Node<'a> { Node::Distribution(name, _, _) => NodeKey::Distribution(name), } } + + /// Return the [`IndexUrl`] of the distribution, if any. + fn index(&self) -> Option<&IndexUrl> { + match self { + Node::Editable(_, _) => None, + Node::Distribution(_, dist, _) => dist.index(), + } + } } impl Verbatim for Node<'_> { @@ -666,6 +681,8 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> { // Determine the annotation comment and separator (between comment and requirement). let mut annotation = None; + // If enabled, include annotations to indicate the dependencies that requested each + // package (e.g., `# via mypy`). if self.include_annotations { // Display all dependencies. let mut edges = self @@ -720,6 +737,14 @@ impl std::fmt::Display for DisplayResolutionGraph<'_> { // Write the line as is. writeln!(f, "{line}")?; } + + // If enabled, include indexes to indicate which index was used for each package (e.g., + // `# from https://pypi.org/simple`). + if self.include_index_annotation { + if let Some(index) = node.index() { + writeln!(f, "{}", format!(" # from {index}").green())?; + } + } } Ok(()) diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index a635c6ed0..b48d33e60 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -67,6 +67,7 @@ pub(crate) async fn pip_compile( include_index_url: bool, include_find_links: bool, include_marker_expression: bool, + include_index_annotation: bool, index_locations: IndexLocations, index_strategy: IndexStrategy, keyring_provider: KeyringProvider, @@ -501,6 +502,7 @@ pub(crate) async fn pip_compile( generate_hashes, include_extras, include_annotations, + include_index_annotation, annotation_style, ) )?; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 22f318c7f..5b6ec7ebf 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -335,6 +335,10 @@ struct PipCompileArgs { #[clap(long)] no_header: bool, + /// Choose the style of the annotation comments, which indicate the source of each package. + #[clap(long, default_value_t=AnnotationStyle::Split, value_enum)] + annotation_style: AnnotationStyle, + /// Change header comment to reflect custom command wrapping `uv pip compile`. #[clap(long, env = "UV_CUSTOM_COMPILE_COMMAND")] custom_compile_command: Option, @@ -495,9 +499,10 @@ struct PipCompileArgs { #[clap(long, hide = true)] emit_marker_expression: bool, - /// Choose the style of the annotation comments, which indicate the source of each package. - #[clap(long, default_value_t=AnnotationStyle::Split, value_enum)] - annotation_style: AnnotationStyle, + /// Include comment annotations indicating the index used to resolve each package (e.g., + /// `# from https://pypi.org/simple`). + #[clap(long)] + emit_index_annotation: bool, #[command(flatten)] compat_args: compat::PipCompileCompatArgs, @@ -1587,6 +1592,7 @@ async fn run() -> Result { args.emit_index_url, args.emit_find_links, args.emit_marker_expression, + args.emit_index_annotation, index_urls, args.index_strategy, args.keyring_provider, diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 79281c57a..2c153868e 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -7430,3 +7430,149 @@ fn compile_index_url_fallback_prefer_primary() -> Result<()> { Ok(()) } + +/// Ensure that `--emit-index-annotation` prints the index URL for each package. +#[test] +fn emit_index_annotation_pypi_org_simple() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("requests")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .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] --exclude-newer 2024-03-25T00:00:00Z requirements.in --emit-index-annotation + certifi==2024.2.2 + # via requests + # from https://pypi.org/simple + charset-normalizer==3.3.2 + # via requests + # from https://pypi.org/simple + idna==3.6 + # via requests + # from https://pypi.org/simple + requests==2.31.0 + # from https://pypi.org/simple + urllib3==2.2.1 + # via requests + # from https://pypi.org/simple + + ----- stderr ----- + Resolved 5 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Ensure that `--emit-index-annotation` plays nicely with `--no-annotate`. +/// +/// For now, `--no-annotate` doesn't affect `--emit-index-annotation`, in that we still emit the +/// index annotation, and leave `--no-annotate` to only affect the package _source_ annotations. +#[test] +fn emit_index_annotation_no_annotate() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("requests")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--emit-index-annotation") + .arg("--no-annotate"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --emit-index-annotation --no-annotate + certifi==2024.2.2 + # from https://pypi.org/simple + charset-normalizer==3.3.2 + # from https://pypi.org/simple + idna==3.6 + # from https://pypi.org/simple + requests==2.31.0 + # from https://pypi.org/simple + urllib3==2.2.1 + # from https://pypi.org/simple + + ----- stderr ----- + Resolved 5 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Ensure that `--emit-index-annotation` plays nicely with `--annotation-style=line`. +#[test] +fn emit_index_annotation_line() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("requests")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--emit-index-annotation") + .arg("--annotation-style") + .arg("line"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --emit-index-annotation --annotation-style line + certifi==2024.2.2 # via requests + # from https://pypi.org/simple + charset-normalizer==3.3.2 # via requests + # from https://pypi.org/simple + idna==3.6 # via requests + # from https://pypi.org/simple + requests==2.31.0 + # from https://pypi.org/simple + urllib3==2.2.1 # via requests + # from https://pypi.org/simple + + ----- stderr ----- + Resolved 5 packages in [TIME] + "### + ); + + Ok(()) +} + +/// `--emit-index-annotation` where packages are pulled from two distinct indexes. +#[test] +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")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--extra-index-url") + .arg("https://test.pypi.org/simple") + .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] --exclude-newer 2024-03-25T00:00:00Z requirements.in --emit-index-annotation + requests==2.5.4.1 + # from https://test.pypi.org/simple + uv==0.1.24 + # from https://pypi.org/simple + + ----- stderr ----- + Resolved 2 packages in [TIME] + "### + ); + + Ok(()) +} From 44e39bdca307b7d6901ccf3e09dc7f11b16c8c0e Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 10 Apr 2024 11:22:41 -0500 Subject: [PATCH 085/110] Replace Python bootstrapping script with Rust implementation (#2842) See https://github.com/astral-sh/uv/issues/2617 Note this also includes: - #2918 - #2931 (pending) A first step towards Python toolchain management in Rust. First, we add a new crate to manage Python download metadata: - Adds a new `uv-toolchain` crate - Adds Rust structs for Python version download metadata - Duplicates the script which downloads Python version metadata - Adds a script to generate Rust code from the JSON metadata - Adds a utility to download and extract the Python version I explored some alternatives like a build script using things like `serde` and `uneval` to automatically construct the code from our structs but deemed it to heavy. Unlike Rye, I don't generate the Rust directly from the web requests and have an intermediate JSON layer to speed up iteration on the Rust types. Next, we add add a `uv-dev` command `fetch-python` to download Python versions per the bootstrapping script. - Downloads a requested version or reads from `.python-versions` - Extracts to `UV_BOOTSTRAP_DIR` - Links executables for path extension This command is not really intended to be user facing, but it's a good PoC for the `uv-toolchain` API. Hash checking (via the sha256) isn't implemented yet, we can do that in a follow-up. Finally, we remove the `scripts/bootstrap` directory, update CI to use the new command, and update the CONTRIBUTING docs. Screenshot 2024-04-08 at 17 12 15 --- .github/workflows/ci.yml | 28 +- CONTRIBUTING.md | 12 +- Cargo.lock | 32 + Cargo.toml | 1 + crates/uv-dev/Cargo.toml | 9 +- crates/uv-dev/src/fetch_python.rs | 142 + crates/uv-dev/src/main.rs | 5 + crates/uv-extract/src/stream.rs | 1 + crates/uv-interpreter/Cargo.toml | 1 + crates/uv-interpreter/src/find_python.rs | 5 +- crates/uv-interpreter/src/interpreter.rs | 13 + crates/uv-interpreter/src/lib.rs | 2 - crates/uv-toolchain/Cargo.toml | 33 + .../uv-toolchain}/fetch-version-metadata.py | 74 +- .../uv-toolchain/python-version-metadata.json | 4946 +++++---------- crates/uv-toolchain/src/downloads.rs | 438 ++ crates/uv-toolchain/src/find.rs | 110 + crates/uv-toolchain/src/lib.rs | 9 + .../src/python_version.rs | 14 - crates/uv-toolchain/src/python_versions.inc | 5467 +++++++++++++++++ .../src/python_versions.inc.mustache | 26 + .../uv-toolchain/template-version-metadata.py | 99 + crates/uv/Cargo.toml | 1 + crates/uv/src/commands/pip_compile.rs | 3 +- crates/uv/src/main.rs | 2 +- crates/uv/tests/common/mod.rs | 150 +- crates/uv/tests/pip_compile_scenarios.rs | 8 +- crates/uv/tests/pip_sync.rs | 8 +- crates/uv/tests/venv.rs | 22 +- scripts/bootstrap/install.py | 204 - scripts/scenarios/templates/compile.mustache | 8 +- 31 files changed, 8170 insertions(+), 3703 deletions(-) create mode 100644 crates/uv-dev/src/fetch_python.rs create mode 100644 crates/uv-toolchain/Cargo.toml rename {scripts/bootstrap => crates/uv-toolchain}/fetch-version-metadata.py (74%) rename scripts/bootstrap/versions.json => crates/uv-toolchain/python-version-metadata.json (63%) create mode 100644 crates/uv-toolchain/src/downloads.rs create mode 100644 crates/uv-toolchain/src/find.rs create mode 100644 crates/uv-toolchain/src/lib.rs rename crates/{uv-interpreter => uv-toolchain}/src/python_version.rs (92%) create mode 100644 crates/uv-toolchain/src/python_versions.inc create mode 100644 crates/uv-toolchain/src/python_versions.inc.mustache create mode 100644 crates/uv-toolchain/template-version-metadata.py delete mode 100755 scripts/bootstrap/install.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a84265926..221bdacb7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,33 +72,23 @@ jobs: name: "cargo test | ${{ matrix.os }}" steps: - uses: actions/checkout@v4 - - - if: ${{ matrix.os == 'macos' }} - name: "Install bootstrap dependencies" - run: brew install coreutils - - - uses: actions/setup-python@v5 - with: - python-version: "3.12" - - - name: "Install required Python versions" - run: | - python -m pip install "zstandard==0.22.0" - python scripts/bootstrap/install.py - - name: "Install Rust toolchain" run: rustup show - - name: "Install cargo nextest" - uses: taiki-e/install-action@v2 - with: - tool: cargo-nextest - - if: ${{ matrix.os != 'windows' }} uses: rui314/setup-mold@v1 - uses: Swatinem/rust-cache@v2 + - name: "Install required Python versions" + run: | + cargo run -p uv-dev -- fetch-python + + - name: "Install cargo nextest" + uses: taiki-e/install-action@v2 + with: + tool: cargo-nextest + - name: "Cargo test" run: | cargo nextest run --workspace --status-level skip --failure-output immediate-final --no-fail-fast -j 12 --final-status-level slow diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d620e22e5..9341a3972 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -22,12 +22,6 @@ CMake may be installed with Homebrew: brew install cmake ``` -The Python bootstrapping script requires `coreutils` and `zstd`; we recommend installing them with Homebrew: - -```shell -brew install coreutils zstd -``` - See the [Python](#python) section for instructions on installing the Python versions. ### Windows @@ -45,13 +39,13 @@ Testing uv requires multiple specific Python versions. You can install them into `/bin` via our bootstrapping script: ```shell -pipx run scripts/bootstrap/install.py +cargo run -p uv-dev -- fetch-python ``` -Alternatively, you can install `zstandard` from PyPI, then run: +You may need to add the versions to your `PATH`: ```shell -python3.12 scripts/bootstrap/install.py +source .env ``` You can configure the bootstrapping directory with `UV_BOOTSTRAP_DIR`. diff --git a/Cargo.lock b/Cargo.lock index fb858cf41..e87f84ce7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4289,6 +4289,7 @@ dependencies = [ "uv-normalize", "uv-requirements", "uv-resolver", + "uv-toolchain", "uv-types", "uv-virtualenv", "uv-warnings", @@ -4456,26 +4457,33 @@ dependencies = [ "pep508_rs", "petgraph", "poloto", + "reqwest", "resvg", "rustc-hash", "serde", "serde_json", "tagu", + "tempfile", "tikv-jemallocator", "tokio", + "tokio-util", "tracing", "tracing-durations-export", "tracing-indicatif", "tracing-subscriber", + "url", "uv-build", "uv-cache", "uv-client", "uv-configuration", "uv-dispatch", + "uv-extract", + "uv-fs", "uv-installer", "uv-interpreter", "uv-normalize", "uv-resolver", + "uv-toolchain", "uv-types", "walkdir", ] @@ -4662,6 +4670,7 @@ dependencies = [ "tracing", "uv-cache", "uv-fs", + "uv-toolchain", "which", "winapi", ] @@ -4754,6 +4763,29 @@ dependencies = [ "uv-warnings", ] +[[package]] +name = "uv-toolchain" +version = "0.1.0" +dependencies = [ + "anyhow", + "fs-err", + "futures", + "once_cell", + "pep440_rs", + "pep508_rs", + "reqwest", + "reqwest-middleware", + "tempfile", + "thiserror", + "tokio", + "tokio-util", + "tracing", + "url", + "uv-client", + "uv-extract", + "uv-fs", +] + [[package]] name = "uv-types" version = "0.0.1" diff --git a/Cargo.toml b/Cargo.toml index 2826ddc7a..8da70b7ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -49,6 +49,7 @@ uv-trampoline = { path = "crates/uv-trampoline" } uv-version = { path = "crates/uv-version" } uv-virtualenv = { path = "crates/uv-virtualenv" } uv-warnings = { path = "crates/uv-warnings" } +uv-toolchain = { path = "crates/uv-toolchain" } anstream = { version = "0.6.13" } anyhow = { version = "1.0.80" } diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index 764bdd3fc..ab56cb0ec 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -23,13 +23,16 @@ pep508_rs = { workspace = true } uv-build = { workspace = true } uv-cache = { workspace = true, features = ["clap"] } uv-client = { workspace = true } +uv-configuration = { workspace = true } uv-dispatch = { workspace = true } +uv-extract = { workspace = true } +uv-fs = { workspace = true } uv-installer = { workspace = true } uv-interpreter = { workspace = true } uv-normalize = { workspace = true } uv-resolver = { workspace = true } +uv-toolchain = { workspace = true } uv-types = { workspace = true } -uv-configuration = { workspace = true } # Any dependencies that are exclusively used in `uv-dev` should be listed as non-workspace # dependencies, to ensure that we're forced to think twice before including them in other crates. @@ -46,14 +49,18 @@ petgraph = { workspace = true } poloto = { version = "19.1.2" } resvg = { version = "0.29.0" } rustc-hash = { workspace = true } +reqwest = { workspace = true } +tempfile = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tagu = { version = "0.1.6" } tokio = { workspace = true } +tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } tracing-durations-export = { workspace = true, features = ["plot"] } tracing-indicatif = { workspace = true } tracing-subscriber = { workspace = true } +url = { workspace = true } walkdir = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] diff --git a/crates/uv-dev/src/fetch_python.rs b/crates/uv-dev/src/fetch_python.rs new file mode 100644 index 000000000..cdbdfcea0 --- /dev/null +++ b/crates/uv-dev/src/fetch_python.rs @@ -0,0 +1,142 @@ +use anyhow::Result; +use clap::Parser; +use fs_err as fs; +#[cfg(unix)] +use fs_err::tokio::symlink; +use futures::StreamExt; +#[cfg(unix)] +use itertools::Itertools; +use std::str::FromStr; +#[cfg(unix)] +use std::{collections::HashMap, path::PathBuf}; +use tokio::time::Instant; +use tracing::{info, info_span, Instrument}; + +use uv_fs::Simplified; +use uv_toolchain::{ + DownloadResult, Error, PythonDownload, PythonDownloadRequest, TOOLCHAIN_DIRECTORY, +}; + +#[derive(Parser, Debug)] +pub(crate) struct FetchPythonArgs { + versions: Vec, +} + +pub(crate) async fn fetch_python(args: FetchPythonArgs) -> Result<()> { + let start = Instant::now(); + + let bootstrap_dir = &*TOOLCHAIN_DIRECTORY; + + fs_err::create_dir_all(bootstrap_dir)?; + + let versions = if args.versions.is_empty() { + info!("Reading versions from file..."); + read_versions_file().await? + } else { + args.versions + }; + + let requests = versions + .iter() + .map(|version| { + PythonDownloadRequest::from_str(version).and_then(PythonDownloadRequest::fill) + }) + .collect::, Error>>()?; + + let downloads = requests + .iter() + .map(|request| match PythonDownload::from_request(request) { + Some(download) => download, + None => panic!("No download found for request {request:?}"), + }) + .collect::>(); + + let client = uv_client::BaseClientBuilder::new().build(); + + info!("Fetching requested versions..."); + let mut tasks = futures::stream::iter(downloads.iter()) + .map(|download| { + async { + let result = download.fetch(&client, bootstrap_dir).await; + (download.python_version(), result) + } + .instrument(info_span!("download", key = %download)) + }) + .buffered(4); + + let mut results = Vec::new(); + let mut downloaded = 0; + while let Some(task) = tasks.next().await { + let (version, result) = task; + let path = match result? { + DownloadResult::AlreadyAvailable(path) => { + info!("Found existing download for v{}", version); + path + } + DownloadResult::Fetched(path) => { + info!("Downloaded v{} to {}", version, path.user_display()); + downloaded += 1; + path + } + }; + results.push((version, path)); + } + + if downloaded > 0 { + let s = if downloaded == 1 { "" } else { "s" }; + info!( + "Fetched {} in {}s", + format!("{} version{}", downloaded, s), + start.elapsed().as_secs() + ); + } else { + info!("All versions downloaded already."); + }; + + // Order matters here, as we overwrite previous links + info!("Installing to `{}`...", bootstrap_dir.user_display()); + + // On Windows, linking the executable generally results in broken installations + // and each toolchain path will need to be added to the PATH separately in the + // desired order + #[cfg(unix)] + { + let mut links: HashMap = HashMap::new(); + for (version, path) in results { + // TODO(zanieb): This path should be a part of the download metadata + let executable = path.join("install").join("bin").join("python3"); + for target in [ + bootstrap_dir.join(format!("python{}", version.python_full_version())), + bootstrap_dir.join(format!("python{}.{}", version.major(), version.minor())), + bootstrap_dir.join(format!("python{}", version.major())), + bootstrap_dir.join("python"), + ] { + // Attempt to remove it, we'll fail on link if we couldn't remove it for some reason + // but if it's missing we don't want to error + let _ = fs::remove_file(&target); + symlink(&executable, &target).await?; + links.insert(target, executable.clone()); + } + } + for (target, executable) in links.iter().sorted() { + info!( + "Linked `{}` to `{}`", + target.user_display(), + executable.user_display() + ); + } + }; + + info!("Installed {} versions", requests.len()); + + Ok(()) +} + +async fn read_versions_file() -> Result> { + let lines: Vec = fs::tokio::read_to_string(".python-versions") + .await? + .lines() + .map(ToString::to_string) + .collect(); + Ok(lines) +} diff --git a/crates/uv-dev/src/main.rs b/crates/uv-dev/src/main.rs index dc5865c9f..ce83a8167 100644 --- a/crates/uv-dev/src/main.rs +++ b/crates/uv-dev/src/main.rs @@ -21,6 +21,7 @@ use resolve_many::ResolveManyArgs; use crate::build::{build, BuildArgs}; use crate::clear_compile::ClearCompileArgs; use crate::compile::CompileArgs; +use crate::fetch_python::FetchPythonArgs; use crate::render_benchmarks::RenderBenchmarksArgs; use crate::resolve_cli::ResolveCliArgs; use crate::wheel_metadata::WheelMetadataArgs; @@ -44,6 +45,7 @@ static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; mod build; mod clear_compile; mod compile; +mod fetch_python; mod render_benchmarks; mod resolve_cli; mod resolve_many; @@ -72,6 +74,8 @@ enum Cli { Compile(CompileArgs), /// Remove all `.pyc` in the tree. ClearCompile(ClearCompileArgs), + /// Fetch Python versions for testing + FetchPython(FetchPythonArgs), } #[instrument] // Anchor span to check for overhead @@ -92,6 +96,7 @@ async fn run() -> Result<()> { Cli::RenderBenchmarks(args) => render_benchmarks::render_benchmarks(&args)?, Cli::Compile(args) => compile::compile(args).await?, Cli::ClearCompile(args) => clear_compile::clear_compile(&args)?, + Cli::FetchPython(args) => fetch_python::fetch_python(args).await?, } Ok(()) } diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index cff2825dc..f9ac12148 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -157,6 +157,7 @@ pub async fn untar_gz( ) -> Result<(), Error> { let reader = tokio::io::BufReader::new(reader); let decompressed_bytes = async_compression::tokio::bufread::GzipDecoder::new(reader); + let mut archive = tokio_tar::ArchiveBuilder::new(decompressed_bytes) .set_preserve_mtime(false) .build(); diff --git a/crates/uv-interpreter/Cargo.toml b/crates/uv-interpreter/Cargo.toml index d8ae8fb75..eef17ce3b 100644 --- a/crates/uv-interpreter/Cargo.toml +++ b/crates/uv-interpreter/Cargo.toml @@ -21,6 +21,7 @@ platform-tags = { workspace = true } pypi-types = { workspace = true } uv-cache = { workspace = true } uv-fs = { workspace = true } +uv-toolchain = { workspace = true } configparser = { workspace = true } fs-err = { workspace = true, features = ["tokio"] } diff --git a/crates/uv-interpreter/src/find_python.rs b/crates/uv-interpreter/src/find_python.rs index b06472d21..2fc45bdec 100644 --- a/crates/uv-interpreter/src/find_python.rs +++ b/crates/uv-interpreter/src/find_python.rs @@ -7,10 +7,11 @@ use tracing::{debug, instrument}; use uv_cache::Cache; use uv_fs::normalize_path; +use uv_toolchain::PythonVersion; use crate::interpreter::InterpreterInfoError; use crate::python_environment::{detect_python_executable, detect_virtual_env}; -use crate::{Error, Interpreter, PythonVersion}; +use crate::{Error, Interpreter}; /// Find a Python of a specific version, a binary with a name or a path to a binary. /// @@ -464,7 +465,7 @@ fn find_version( let version_matches = |interpreter: &Interpreter| -> bool { if let Some(python_version) = python_version { // If a patch version was provided, check for an exact match - python_version.is_satisfied_by(interpreter) + interpreter.satisfies(python_version) } else { // The version always matches if one was not provided true diff --git a/crates/uv-interpreter/src/interpreter.rs b/crates/uv-interpreter/src/interpreter.rs index a5ff8e01a..ce14845f9 100644 --- a/crates/uv-interpreter/src/interpreter.rs +++ b/crates/uv-interpreter/src/interpreter.rs @@ -16,6 +16,7 @@ use platform_tags::{Tags, TagsError}; use pypi_types::Scheme; use uv_cache::{Cache, CacheBucket, CachedByTimestamp, Freshness, Timestamp}; use uv_fs::{write_atomic_sync, PythonExt, Simplified}; +use uv_toolchain::PythonVersion; use crate::Error; use crate::Virtualenv; @@ -314,6 +315,18 @@ impl Interpreter { }, } } + + /// Check if the interpreter matches the given Python version. + /// + /// If a patch version is present, we will require an exact match. + /// Otherwise, just the major and minor version numbers need to match. + pub fn satisfies(&self, version: &PythonVersion) -> bool { + if version.patch().is_some() { + version.version() == self.python_version() + } else { + (version.major(), version.minor()) == self.python_tuple() + } + } } /// The `EXTERNALLY-MANAGED` file in a Python installation. diff --git a/crates/uv-interpreter/src/lib.rs b/crates/uv-interpreter/src/lib.rs index 535888a78..1286ff436 100644 --- a/crates/uv-interpreter/src/lib.rs +++ b/crates/uv-interpreter/src/lib.rs @@ -19,14 +19,12 @@ pub use crate::find_python::{find_best_python, find_default_python, find_request pub use crate::interpreter::Interpreter; use crate::interpreter::InterpreterInfoError; pub use crate::python_environment::PythonEnvironment; -pub use crate::python_version::PythonVersion; pub use crate::virtualenv::Virtualenv; mod cfg; mod find_python; mod interpreter; mod python_environment; -mod python_version; mod virtualenv; #[derive(Debug, Error)] diff --git a/crates/uv-toolchain/Cargo.toml b/crates/uv-toolchain/Cargo.toml new file mode 100644 index 000000000..59bab562e --- /dev/null +++ b/crates/uv-toolchain/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "uv-toolchain" +version = "0.1.0" +edition.workspace = true +rust-version.workspace = true +homepage.workspace = true +documentation.workspace = true +repository.workspace = true +authors.workspace = true +license.workspace = true + +[dependencies] +uv-client = { workspace = true } +uv-extract = { workspace = true } +uv-fs = { workspace = true } +pep440_rs = { workspace = true } +pep508_rs = { workspace = true } + +anyhow = { workspace = true } +fs-err = { workspace = true } +futures = { workspace = true } +once_cell = {workspace = true} +reqwest = { workspace = true } +reqwest-middleware = { workspace = true } +tempfile = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tokio-util = { workspace = true, features = ["compat"] } +tracing = { workspace = true } +url = { workspace = true } + +[lints] +workspace = true diff --git a/scripts/bootstrap/fetch-version-metadata.py b/crates/uv-toolchain/fetch-version-metadata.py similarity index 74% rename from scripts/bootstrap/fetch-version-metadata.py rename to crates/uv-toolchain/fetch-version-metadata.py index 389269f44..2dc4b3d9a 100755 --- a/scripts/bootstrap/fetch-version-metadata.py +++ b/crates/uv-toolchain/fetch-version-metadata.py @@ -2,7 +2,7 @@ """ Fetch Python version metadata. -Generates the bootstrap `versions.json` file. +Generates the `python-version-metadata.json` file. Usage: @@ -30,7 +30,7 @@ RELEASE_URL = "https://api.github.com/repos/indygreg/python-build-standalone/rel HEADERS = { "X-GitHub-Api-Version": "2022-11-28", } -VERSIONS_FILE = SELF_DIR / "versions.json" +VERSIONS_FILE = SELF_DIR / "python-version-metadata.json" FLAVOR_PREFERENCES = [ "shared-pgo", "shared-noopt", @@ -53,6 +53,8 @@ SPECIAL_TRIPLES = { "linux64": "x86_64-unknown-linux-gnu", "windows-amd64": "x86_64-pc-windows", "windows-x86": "i686-pc-windows", + "windows-amd64-shared": "x86_64-pc-windows", + "windows-x86-shared": "i686-pc-windows", "linux64-musl": "x86_64-unknown-linux-musl", } @@ -78,8 +80,14 @@ _suffix_re = re.compile( ) ) -# to match the output of the `arch` command -ARCH_MAP = {"aarch64": "arm64"} +# Normalized mappings to match the Rust types +ARCH_MAP = { + "ppc64": "powerpc64", + "ppc64le": "powerpc64le", + "i686": "x86", + "i386": "x86", +} +OS_MAP = {"darwin": "macos"} def parse_filename(filename): @@ -104,10 +112,8 @@ def normalize_triple(triple): triple = SPECIAL_TRIPLES.get(triple, triple) pieces = triple.split("-") try: - arch = pieces[0] - # Normalize - arch = ARCH_MAP.get(arch, arch) - platform = pieces[2] + arch = normalize_arch(pieces[0]) + operating_system = normalize_os(pieces[2]) if pieces[2] == "linux": # On linux, the triple has four segments, the last one is the libc libc = pieces[3] @@ -116,7 +122,18 @@ def normalize_triple(triple): except IndexError: logging.debug("Skipping %r: unknown triple", triple) return - return "%s-%s-%s" % (arch, platform, libc) + return "%s-%s-%s" % (arch, operating_system, libc) + + +def normalize_arch(arch): + arch = ARCH_MAP.get(arch, arch) + pieces = arch.split("_") + # Strip `_vN` from `x86_64` + return "_".join(pieces[:2]) + + +def normalize_os(os): + return OS_MAP.get(os, os) def read_sha256(url): @@ -125,7 +142,7 @@ def read_sha256(url): except urllib.error.HTTPError: return None assert resp.status == 200 - return resp.read().strip() + return resp.read().decode().strip() def sha256(path): @@ -142,8 +159,8 @@ def sha256(path): return h.hexdigest() -def _sort_key(info): - triple, flavor, url = info +def _sort_by_flavor_preference(info): + _triple, flavor, _url = info try: pref = FLAVOR_PREFERENCES.index(flavor) except ValueError: @@ -151,12 +168,18 @@ def _sort_key(info): return pref +def _sort_by_interpreter_and_version(info): + interpreter, version_tuple, _ = info + return (interpreter, version_tuple) + + def find(): """ Find available Python versions and write metadata to a file. """ results = {} + # Collect all available Python downloads for page in range(1, 100): logging.debug("Reading release page %s...", page) resp = urllib.request.urlopen("%s?page=%d" % (RELEASE_URL, page)) @@ -180,34 +203,47 @@ def find(): continue results.setdefault(py_ver, []).append((triple, flavor, url)) - cpython_results = {} + # Collapse CPython variants to a single URL flavor per triple + cpython_results: dict[tuple[int, int, int], dict[tuple[str, str, str], str]] = {} for py_ver, choices in results.items(): - choices.sort(key=_sort_key) urls = {} - for triple, flavor, url in choices: + for triple, flavor, url in sorted(choices, key=_sort_by_flavor_preference): triple = tuple(triple.split("-")) + # Skip existing triples, preferring the first flavor if triple in urls: continue urls[triple] = url cpython_results[tuple(map(int, py_ver.split(".")))] = urls + # Collect variants across interpreter kinds + # TODO(zanieb): Note we only support CPython downloads at this time + # but this will include PyPy chain in the future. final_results = {} for interpreter, py_ver, choices in sorted( chain( (("cpython",) + x for x in cpython_results.items()), ), - key=lambda x: x[:2], + key=_sort_by_interpreter_and_version, + # Reverse the ordering so newer versions are first reverse=True, ): - for (arch, platform, libc), url in sorted(choices.items()): - key = "%s-%s.%s.%s-%s-%s-%s" % (interpreter, *py_ver, platform, arch, libc) + # Sort by the remaining information for determinism + # This groups download metadata in triple component order + for (arch, operating_system, libc), url in sorted(choices.items()): + key = "%s-%s.%s.%s-%s-%s-%s" % ( + interpreter, + *py_ver, + operating_system, + arch, + libc, + ) logging.info("Found %s", key) sha256 = read_sha256(url) final_results[key] = { "name": interpreter, "arch": arch, - "os": platform, + "os": operating_system, "libc": libc, "major": py_ver[0], "minor": py_ver[1], diff --git a/scripts/bootstrap/versions.json b/crates/uv-toolchain/python-version-metadata.json similarity index 63% rename from scripts/bootstrap/versions.json rename to crates/uv-toolchain/python-version-metadata.json index c95201415..3306ca69d 100644 --- a/scripts/bootstrap/versions.json +++ b/crates/uv-toolchain/python-version-metadata.json @@ -1,18 +1,106 @@ { - "cpython-3.12.1-darwin-arm64-none": { + "cpython-3.12.2-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 12, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "469a7fd0d0a09936c5db41b5ac83bb29d5bfeb721aa483ac92f3f7ac4d311097" + }, + "cpython-3.12.2-macos-aarch64-none": { + "name": "cpython", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 12, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "61e51e3490537b800fcefad718157cf775de41044e95aa538b63ab599f66f3a9" + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "2afcc8b25c55793f6ceb0bef2e547e101f53c9e25a0fe0332320e5381a1f0fdb" }, - "cpython-3.12.1-linux-arm64-gnu": { + "cpython-3.12.2-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "arm64", + "arch": "powerpc64le", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 12, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "1d70476fb9013cc93e787417680b34629b510e6e2145cf48bb2f0fe887f7a4d8" + }, + "cpython-3.12.2-linux-s390x-gnu": { + "name": "cpython", + "arch": "s390x", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 12, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "f40b88607928b5ee34ff87c1d574c8493a1604d7a40474e1b03731184186f419" + }, + "cpython-3.12.2-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", + "libc": "none", + "major": 3, + "minor": 12, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "ee985ae6a6a98f4d5bd19fd8c59f45235911d19b64e1dbd026261b8103f15db5" + }, + "cpython-3.12.2-linux-x86_64-gnu": { + "name": "cpython", + "arch": "x86_64", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 12, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "15b61ed9d33b35ad014a13a68a55d8ea5ba7fb70945644747f4e53c659f2fed6" + }, + "cpython-3.12.2-linux-x86_64-musl": { + "name": "cpython", + "arch": "x86_64", + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 12, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + "sha256": "7ec1dc7ad8223ec5839a57d232fd3cf730987f7b0f88b2c4f15ee935bcabbaa9" + }, + "cpython-3.12.2-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 12, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "b4b4d19c36e86803aa0b4410395f5568bef28d82666efba926e44dbe06345a12" + }, + "cpython-3.12.2-windows-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "windows", + "libc": "none", + "major": 3, + "minor": 12, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "a1daf5e8ceb23d34ea29b16b5123b06694810fe7acc5c8384426435c63bf731e" + }, + "cpython-3.12.1-linux-aarch64-gnu": { + "name": "cpython", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -21,20 +109,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "9009da24f436611d0bf086b8ea62aaed1c27104af5b770ddcfc92b60db06da8c" }, - "cpython-3.12.1-windows-i686-none": { + "cpython-3.12.1-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 12, "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "22866d35fdf58e90e75d6ba9aa78c288b452ea7041fa9bc5549eca9daa431883" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "61e51e3490537b800fcefad718157cf775de41044e95aa538b63ab599f66f3a9" }, - "cpython-3.12.1-linux-ppc64le-gnu": { + "cpython-3.12.1-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -54,16 +142,16 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-s390x-unknown-linux-gnu-debug-full.tar.zst", "sha256": "505a4fbace661a43b354a059022eb31efb406859a5f7227109ebf0f278f20503" }, - "cpython-3.12.1-darwin-x86_64-none": { + "cpython-3.12.1-windows-x86-none": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 12, "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "bf2b176b0426d7b4d4909c1b19bbb25b4893f9ebdc61e32df144df2b10dcc800" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "22866d35fdf58e90e75d6ba9aa78c288b452ea7041fa9bc5549eca9daa431883" }, "cpython-3.12.1-linux-x86_64-gnu": { "name": "cpython", @@ -87,6 +175,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "c4b07a02d8f0986b56e010a67132e5eeba1def4991c6c06ed184f831a484a06f" }, + "cpython-3.12.1-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 12, + "patch": 1, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "bf2b176b0426d7b4d4909c1b19bbb25b4893f9ebdc61e32df144df2b10dcc800" + }, "cpython-3.12.1-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -98,86 +197,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "d9bc1b566250bf51818976bf98bf50e1f4c59b2503b50d29250cac5ab5ef6b38" }, - "cpython-3.12.1-linux-x86_64_v2-gnu": { + "cpython-3.12.0-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 12, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "21a5182c499954bde10344c7cc3ba9f69a39f0b485a9420871bdf65f26587bb7" - }, - "cpython-3.12.1-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 12, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "47a85a6f99be6ec1746e25a5002a1b942d26036b78f32f6c26ff1285196b2411" - }, - "cpython-3.12.1-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 12, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "d2088f53a3e160973ec34376c5a8bc4f430626ea154a57a8ae868f37b43320f3" - }, - "cpython-3.12.1-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 12, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "24f76157c615f2cf6a92437290f711b27cd5b29bcea4f17b50ee83920f307e2a" - }, - "cpython-3.12.1-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 12, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b5c640ffdde33f3d333ed772878694c3be79caf5707de3da23aa8f77cfad4164" - }, - "cpython-3.12.1-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 12, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "6e521b73faee3f44161db17f8cd89599c54fbf28f28de215851f9b9e7ded8b75" - }, - "cpython-3.12.0-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 12, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "25fc8cd41e975d18d13bcc8f8beffa096ff8a0b86c4a737e1c6617900092c966" - }, - "cpython-3.12.0-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -186,20 +208,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "eb05c976374a9a44596ce340ab35e5461014f30202c3cbe10edcbfbe5ac4a6a1" }, - "cpython-3.12.0-windows-i686-none": { + "cpython-3.12.0-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 12, "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "465e91b6e6d0d1c40c8a4bce3642c4adcb9b75cf03fbd5fd5a33a36358249289" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "25fc8cd41e975d18d13bcc8f8beffa096ff8a0b86c4a737e1c6617900092c966" }, - "cpython-3.12.0-linux-ppc64le-gnu": { + "cpython-3.12.0-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -219,16 +241,16 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-s390x-unknown-linux-gnu-debug-full.tar.zst", "sha256": "5b1a1effbb43df57ad014fcebf4b20089e504d89613e7b8db22d9ccb9fb00a6c" }, - "cpython-3.12.0-darwin-x86_64-none": { + "cpython-3.12.0-windows-x86-none": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 12, "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "3b4781e7fd4efabe574ba0954e54c35c7d5ac4dc5b2990b40796c1c6aec67d79" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "465e91b6e6d0d1c40c8a4bce3642c4adcb9b75cf03fbd5fd5a33a36358249289" }, "cpython-3.12.0-linux-x86_64-gnu": { "name": "cpython", @@ -252,6 +274,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "91b42595cb4b69ff396e746dc492caf67b952a3ed1a367a4ace1acc965ed9cdb" }, + "cpython-3.12.0-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 12, + "patch": 0, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "3b4781e7fd4efabe574ba0954e54c35c7d5ac4dc5b2990b40796c1c6aec67d79" + }, "cpython-3.12.0-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -263,86 +296,108 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "5bdff7ed56550d96f9b26a27a8c25f0cc58a03bff19e5f52bba84366183cab8b" }, - "cpython-3.12.0-linux-x86_64_v2-gnu": { + "cpython-3.11.8-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, - "minor": 12, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "572f8559f0e8a086c4380ea4417d44f6f3751afd18d01e14e04099ec33e1b199" + "minor": 11, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "45bf082aca6b7d5e7261852720a72b92f5305e9fdb07b10f6588cb51d8f83ff2" }, - "cpython-3.12.0-linux-x86_64_v2-musl": { + "cpython-3.11.8-macos-aarch64-none": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 12, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "ec028edc5fcca34b5c5988074e44d91158d924695f26ca5885c6b4a1daa6349d" - }, - "cpython-3.12.0-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 12, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "1ae35f54bad2b473298858a3cb7bf811291fdd40c8159eff4ff593168ed8765e" - }, - "cpython-3.12.0-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 12, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "154cc904e028f93fa5b6dfb23aa3d9241db5c210c99a8facab9425e6a5f54a31" - }, - "cpython-3.12.0-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 12, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3c900c3495453cfa7e7626026ef0d8be3adf589b2b810969f6a9f44dba3c129d" - }, - "cpython-3.12.0-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 12, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "7f850e95a29b43dff3656e3d403d98c30049a3a90a74aac1c6763339c6d26632" - }, - "cpython-3.11.7-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 11, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "c1f3dd13825906a5eae23ed8de9b653edb620568b2e0226eef3784eb1cce7eed" + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "c0650884b929253b8688797d1955850f6e339bf0428b3d935f62ab3159f66362" }, - "cpython-3.11.7-linux-arm64-gnu": { + "cpython-3.11.8-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "arm64", + "arch": "powerpc64le", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 11, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "a9716f2eebebe03de47d6d5d603d6ff78abf5eb38f88bf7607b17fd85e74ff16" + }, + "cpython-3.11.8-linux-s390x-gnu": { + "name": "cpython", + "arch": "s390x", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 11, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "d495830b5980ed689bd7588aa556bac9c43ff766d8a8b32e7791b8ed664b04f3" + }, + "cpython-3.11.8-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "c3e90962996177a027bd73dd9fd8c42a2d6ef832cda26db4ab4efc6105160537" + }, + "cpython-3.11.8-linux-x86_64-gnu": { + "name": "cpython", + "arch": "x86_64", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 11, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "d959c43184878d564b5368ce4d753cf059600aafdf3e50280e850f94b5a4ba61" + }, + "cpython-3.11.8-linux-x86_64-musl": { + "name": "cpython", + "arch": "x86_64", + "os": "linux", + "libc": "musl", + "major": 3, + "minor": 11, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + "sha256": "a03a9d8c1f770ce418716a2e8185df7b3a9e0012cdc220f9f2d24480a432650b" + }, + "cpython-3.11.8-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "54f8c8ad7313b3505e495bb093825d85eab244306ca4278836a2c7b5b74fb053" + }, + "cpython-3.11.8-windows-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "windows", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "6da82390f7ac49f6c4b19a5b8019c4ddc1eef2c5ad6a2f2d32773a27663a4e14" + }, + "cpython-3.11.7-linux-aarch64-gnu": { + "name": "cpython", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -351,20 +406,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "e3a375f8f16198ccf8dbede231536544265e5b4b6b0f0df97c5b29503c5864e2" }, - "cpython-3.11.7-windows-i686-none": { + "cpython-3.11.7-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 11, "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "6613f1f9238d19969d8a2827deec84611cb772503207056cc9f0deb89bea48cd" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "c1f3dd13825906a5eae23ed8de9b653edb620568b2e0226eef3784eb1cce7eed" }, - "cpython-3.11.7-linux-ppc64le-gnu": { + "cpython-3.11.7-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -384,16 +439,16 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-s390x-unknown-linux-gnu-debug-full.tar.zst", "sha256": "91b33369025b7e0079f603cd2a99f9a5932daa8ded113d5090f29c075c993df7" }, - "cpython-3.11.7-darwin-x86_64-none": { + "cpython-3.11.7-windows-x86-none": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 11, "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "3f8caf73f2bfe22efa9666974c119727e163716e88af8ed3caa1e0ae5493de61" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "6613f1f9238d19969d8a2827deec84611cb772503207056cc9f0deb89bea48cd" }, "cpython-3.11.7-linux-x86_64-gnu": { "name": "cpython", @@ -417,6 +472,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "f387d373d64447bbba8a5657712f93b1dbdfd7246cdfe5a0493f39b83d46ec7c" }, + "cpython-3.11.7-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 7, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "3f8caf73f2bfe22efa9666974c119727e163716e88af8ed3caa1e0ae5493de61" + }, "cpython-3.11.7-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -428,86 +494,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "89d1d8f080e5494ea57918fc5ecf3d483ffef943cd5a336e64da150cd44b4aa0" }, - "cpython-3.11.7-linux-x86_64_v2-gnu": { + "cpython-3.11.6-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "24dba70107ca3999c0a2742b3bf898f740a063736f3cd208e80e056adf19cd7f" - }, - "cpython-3.11.7-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "a6c43c58ff21316242bd43efa90837f4a344e64ba4f4306c5ddc49005bfc1dd3" - }, - "cpython-3.11.7-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "6f5246b7cb8cc36a98025c70f829d2e5d197a7d20d51f21ca44b1e4242b13f0d" - }, - "cpython-3.11.7-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "23af703408cb9a91eb2e673640576524ba616d8d1f8af079d22e67192e174400" - }, - "cpython-3.11.7-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "74e5053f3f40d4ea018d79139d8a739c0ab0d457b8a9f1597a160bd88fbd58ab" - }, - "cpython-3.11.7-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "63d3cd05a6c9e7597d1c2ec3d02473170a4f2eb4acf5558974c639cf787a44a1" - }, - "cpython-3.11.6-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "6e9007bcbbf51203e89c34a87ed42561630a35bc4eb04a565c92ba7159fe5826" - }, - "cpython-3.11.6-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -516,20 +505,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "d63d6eb065e60899b25853fe6bbd9f60ea6c3b12f4854adc75cb818bad55f4e9" }, - "cpython-3.11.6-windows-i686-none": { + "cpython-3.11.6-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 11, "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "2670731428191d4476bf260c8144ccf06f9e5f8ac6f2de1dc444ca96ab627082" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "6e9007bcbbf51203e89c34a87ed42561630a35bc4eb04a565c92ba7159fe5826" }, - "cpython-3.11.6-linux-ppc64le-gnu": { + "cpython-3.11.6-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -549,16 +538,16 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-s390x-unknown-linux-gnu-debug-full.tar.zst", "sha256": "78252aa883fed18de7bb9b146450e42dd75d78c345f56c1301bb042317a1d4f7" }, - "cpython-3.11.6-darwin-x86_64-none": { + "cpython-3.11.6-windows-x86-none": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 11, "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "3685156e4139e89484c071ba1a1b85be0b4e302a786de5a170d3b0713863c2e8" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "2670731428191d4476bf260c8144ccf06f9e5f8ac6f2de1dc444ca96ab627082" }, "cpython-3.11.6-linux-x86_64-gnu": { "name": "cpython", @@ -582,6 +571,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "1b6e32ec93c5a18a03a9da9e2a3a3738d67b733df0795edcff9fd749c33ab931" }, + "cpython-3.11.6-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 6, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "3685156e4139e89484c071ba1a1b85be0b4e302a786de5a170d3b0713863c2e8" + }, "cpython-3.11.6-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -593,86 +593,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "38d2c2fa2f9effbf486207bef7141d1b5c385ad30729ab0c976e6a852a2a9401" }, - "cpython-3.11.6-linux-x86_64_v2-gnu": { + "cpython-3.11.5-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2ef8df0de9c400b5d57971fe5bcff36b4dca2410504a9edbd407572ea61044e0" - }, - "cpython-3.11.6-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "a626529e7ebe28755090d16d22fc3f8b85dd7d19ab9791d224336f002b9b51a5" - }, - "cpython-3.11.6-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "d96c26d88966873184fc0ee99ca7b941d274b669b1b11e185749fc065d12908f" - }, - "cpython-3.11.6-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "cec6af027d49c6218680d78960392c4853d74d2f0244ac374ed216ed57f3a79b" - }, - "cpython-3.11.6-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "865506f3eb1ff9a25f458c1b46d4fe6ceffac869ca01c203c258e3563c87630e" - }, - "cpython-3.11.6-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "f6fb43020a46b6f82c552f95db1170c15139d2b6542d38e08ea915902e9da961" - }, - "cpython-3.11.5-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "7bee180b764722a73c2599fbe2c3a6121cf6bbcb08cb3082851e93c43fe130e7" - }, - "cpython-3.11.5-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -681,31 +604,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "ac4b1e91d1cb7027595bfa4667090406331b291b2e346fb74e42b7031b216787" }, - "cpython-3.11.5-linux-i686-gnu": { + "cpython-3.11.5-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "75d27b399b323c25d8250fda9857e388bf1b03ba1eb7925ec23cf12042a63a88" - }, - "cpython-3.11.5-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 11, "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "c9ffe9c2c88685ce3064f734cbdfede0a07de7d826fada58f8045f3bd8f81a9d" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "7bee180b764722a73c2599fbe2c3a6121cf6bbcb08cb3082851e93c43fe130e7" }, - "cpython-3.11.5-linux-ppc64le-gnu": { + "cpython-3.11.5-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -725,16 +637,27 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-s390x-unknown-linux-gnu-debug-full.tar.zst", "sha256": "b0819032ec336d6e1d9e9bfdba546bf854a7b7248f8720a6d07da72c4ac927e5" }, - "cpython-3.11.5-darwin-x86_64-none": { + "cpython-3.11.5-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 11, + "patch": 5, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "75d27b399b323c25d8250fda9857e388bf1b03ba1eb7925ec23cf12042a63a88" + }, + "cpython-3.11.5-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 11, "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "e43d70a49919641ca2939a5a9107b13d5fef8c13af0f511a33a94bb6af2044f0" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "c9ffe9c2c88685ce3064f734cbdfede0a07de7d826fada58f8045f3bd8f81a9d" }, "cpython-3.11.5-linux-x86_64-gnu": { "name": "cpython", @@ -758,6 +681,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "9dcf19ee54fb936cb9fd0f02fd655e790663534bc12e142e460c1b30a0b54dbd" }, + "cpython-3.11.5-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 5, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "e43d70a49919641ca2939a5a9107b13d5fef8c13af0f511a33a94bb6af2044f0" + }, "cpython-3.11.5-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -769,86 +703,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "6e4d20e6d498f9edeb3c28cb9541ad20f675f16da350b078e40a9dcfd93cdc3d" }, - "cpython-3.11.5-linux-x86_64_v2-gnu": { + "cpython-3.11.4-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "d7f5abc89c66a8a1d086394c80a94a17b5b26887983dbf5a80998302f3626ab2" - }, - "cpython-3.11.5-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "bbd36a2ce9896b3f9c3fd69c8d58477bf34a08bc92276ea4ca90551071017cae" - }, - "cpython-3.11.5-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "292c37355054d6f4f864b4e772d8bd23541b744d73b3293523f461dcfcad2750" - }, - "cpython-3.11.5-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "9d697a4832d03c7c41bcde53a90d606af7804e4c142a2882e8e237fa628eecf0" - }, - "cpython-3.11.5-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "dfa7e0d8fbcde146c5a952f8fa8a1a1121d4b64e7e9e0153d81a06d149a101d3" - }, - "cpython-3.11.5-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "0c10a26ab61cff71fbd64952c669d414f0fa050c8a77de128d2bbf6eec240a32" - }, - "cpython-3.11.4-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "988d476c806f71a3233ff4266eda166a5d28cf83ba306ac88b4220554fc83e8c" - }, - "cpython-3.11.4-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -857,31 +714,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "37cf00439b57adf7ffef4a349d62dcf09739ba67b670e903b00b25f81fbb8a68" }, - "cpython-3.11.4-linux-i686-gnu": { + "cpython-3.11.4-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a9051364b5c2e28205f8484cae03d16c86b45df5d117324e846d0f5e870fe9fb" - }, - "cpython-3.11.4-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 11, "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "0d22f43c5bb3f27ff2f9e8c60b0d7abd391bb2cac1790b0960970ff5580f6e9a" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "988d476c806f71a3233ff4266eda166a5d28cf83ba306ac88b4220554fc83e8c" }, - "cpython-3.11.4-linux-ppc64le-gnu": { + "cpython-3.11.4-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -901,16 +747,27 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-s390x-unknown-linux-gnu-debug-full.tar.zst", "sha256": "8ef6b5fa86b4abf51865b346b7cf8df36e474ed308869fc0ac3fe82de39194a4" }, - "cpython-3.11.4-darwin-x86_64-none": { + "cpython-3.11.4-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 11, + "patch": 4, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "a9051364b5c2e28205f8484cae03d16c86b45df5d117324e846d0f5e870fe9fb" + }, + "cpython-3.11.4-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 11, "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "6d9765785316c7f1c07def71b413c92c84302f798b30ee09e2e0b5da28353a51" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "0d22f43c5bb3f27ff2f9e8c60b0d7abd391bb2cac1790b0960970ff5580f6e9a" }, "cpython-3.11.4-linux-x86_64-gnu": { "name": "cpython", @@ -934,6 +791,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "fc2ea02ced875c90b8d025b409d58c4f045df8ba951bfa2b8b0a3cfe11c3b41c" }, + "cpython-3.11.4-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 4, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "6d9765785316c7f1c07def71b413c92c84302f798b30ee09e2e0b5da28353a51" + }, "cpython-3.11.4-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -945,86 +813,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "1692d795d6199b2261161ae54250009ffad0317929302903f6f2c773befd4d76" }, - "cpython-3.11.4-linux-x86_64_v2-gnu": { + "cpython-3.11.3-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "95ac78a3d834ce1feaa45bfe4997563df0bc65c3d2711a5d6d4f39d3c240ffd4" - }, - "cpython-3.11.4-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "54f9206357c118b4ea26f0e3ebe9a558153fff8c42ff4c8546d4c8464956401b" - }, - "cpython-3.11.4-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "85859b2993d3c52506625e1dc62b0cb4d9e38816e290d7630fe258aae6b8bca4" - }, - "cpython-3.11.4-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "b95e54792e8db7531a1ad23d574a0fb4c8510f75d3102b2d41386460345c5383" - }, - "cpython-3.11.4-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a3100464c5f1bc3beb1e15c2bf01ad1aff3888d20a25a1ded1a5baff7be54ff1" - }, - "cpython-3.11.4-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "7e346bf5a96e3719f75889b98baf02e38dd1be812be261e5e5867d357f4f5eab" - }, - "cpython-3.11.3-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "cd296d628ceebf55a78c7f6a7aed379eba9dbd72045d002e1c2c85af0d6f5049" - }, - "cpython-3.11.3-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -1033,31 +824,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "991521082b0347878ba855c4986d77cc805c22ef75159bc95dd24bfd80275e27" }, - "cpython-3.11.3-linux-i686-gnu": { + "cpython-3.11.3-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7bd694eb848328e96f524ded0f9b9eca6230d71fce3cd49b335a5c33450f3e04" - }, - "cpython-3.11.3-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 11, "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "877c90ef778a526aa25ab417034f5e70728ac14e5eb1fa5cfd741f531203a3fc" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "cd296d628ceebf55a78c7f6a7aed379eba9dbd72045d002e1c2c85af0d6f5049" }, - "cpython-3.11.3-linux-ppc64le-gnu": { + "cpython-3.11.3-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -1066,16 +846,27 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-ppc64le-unknown-linux-gnu-debug-full.tar.zst", "sha256": "241d583be3ecc34d76fafa0d186cb504ce5625eb2c0e895dc4f4073a649e5c73" }, - "cpython-3.11.3-darwin-x86_64-none": { + "cpython-3.11.3-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 11, + "patch": 3, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "7bd694eb848328e96f524ded0f9b9eca6230d71fce3cd49b335a5c33450f3e04" + }, + "cpython-3.11.3-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 11, "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "2fbb31a8bc6663e2d31d3054319b51a29b1915c03222a94b9d563233e11d1bef" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "877c90ef778a526aa25ab417034f5e70728ac14e5eb1fa5cfd741f531203a3fc" }, "cpython-3.11.3-linux-x86_64-gnu": { "name": "cpython", @@ -1099,6 +890,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "8c5adef5bc627f39e93b920af86ef740e917aa698530ff727978d446a07bbd8b" }, + "cpython-3.11.3-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 3, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "2fbb31a8bc6663e2d31d3054319b51a29b1915c03222a94b9d563233e11d1bef" + }, "cpython-3.11.3-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -1110,86 +912,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "9d27e607fb1cb2d766e17f27853013d8c0f0b09ac53127aaff03ec89ab13370d" }, - "cpython-3.11.3-linux-x86_64_v2-gnu": { + "cpython-3.11.1-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "f42165d39624cd664ad2c77401b03f2b86c1eaa346a8d97595dc3da9477f7ba7" - }, - "cpython-3.11.3-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "da2b6278ba05879dfa1929c38260d70ee839425fa175f253417370c97974f679" - }, - "cpython-3.11.3-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "9b049c8397cb33d8ee8ce810f971569dbeddc058325120dbc9463efd05fd97f4" - }, - "cpython-3.11.3-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "ba81489c9aa454d8c76c4a0cc12c1a90c66f677f5b4eebc62348a7575983a49b" - }, - "cpython-3.11.3-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8977784dad18e495cfaaaffa4d3196cba76ddcb6ba665375a3c8d707267478b5" - }, - "cpython-3.11.3-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "b985b9b3b57ec919272a01706fb85eef59d4877f6970419160fdf8cfffb4caa8" - }, - "cpython-3.11.1-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "da187194cc351d827232b1d2d85b2855d7e25a4ada3e47bc34b4f87b1d989be5" - }, - "cpython-3.11.1-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -1198,9 +923,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "8fe27d850c02aa7bb34088fad5b48df90b4b841f40e1472243b8ab9da8776e40" }, - "cpython-3.11.1-linux-i686-gnu": { + "cpython-3.11.1-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 1, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "da187194cc351d827232b1d2d85b2855d7e25a4ada3e47bc34b4f87b1d989be5" + }, + "cpython-3.11.1-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -1209,9 +945,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "7986ebe82c07ecd2eb94fd1b3c9ebbb2366db2360e38f29ae0543e857551d0bf" }, - "cpython-3.11.1-windows-i686-none": { + "cpython-3.11.1-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -1220,17 +956,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "b062ac2c72a85510fb9300675bd5c716baede21e9482ef6335247b4aa006584c" }, - "cpython-3.11.1-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 11, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "0eb61be53ee13cf75a30b8a164ef513a2c7995b25b118a3a503245d46231b13a" - }, "cpython-3.11.1-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -1253,6 +978,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "ec5da5b428f6d91d96cde2621c0380f67bb96e4257d2628bc70b50e75ec5f629" }, + "cpython-3.11.1-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 11, + "patch": 1, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "0eb61be53ee13cf75a30b8a164ef513a2c7995b25b118a3a503245d46231b13a" + }, "cpython-3.11.1-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -1264,126 +1000,38 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "f5c46fffda7d7894b975af728f739b02d1cec50fd4a3ea49f69de9ceaae74b17" }, - "cpython-3.11.1-linux-x86_64_v2-gnu": { + "cpython-3.10.13-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, - "minor": 11, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ea2d1de07fbd276723d61cb9f3c8fe4415f1207b3351593a6985e8e4338e89e0" + "minor": 10, + "patch": 13, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "06a53040504e1e2fdcb32dc0d61b123bea76725b5c14031c8f64e28f52ae5a5f" }, - "cpython-3.11.1-linux-x86_64_v2-musl": { + "cpython-3.10.13-macos-aarch64-none": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "8df1c3ff768c308b113e847e64726b5743c45fde3292b3ad60ffbd40e15b4ac7" - }, - "cpython-3.11.1-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "be1259db03ae12ca8c8cdc1a75a3f4aa47579725f2e62f71022f6049690b6498" - }, - "cpython-3.11.1-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "c4f843cf506e3d7e230e2842f2b8e4e8f470ff3c8707e0b70d0d1285bb938986" - }, - "cpython-3.11.1-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 11, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "abf6c9a813e3c600b095ccfe0fb8c21e2b4156df342e9cf4ea34cb5759a0ff1c" - }, - "cpython-3.11.1-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 11, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "9524d8988d552ee6be87bbda76b38431e43c68099fa1ff98368a4fea634e78ff" - }, - "cpython-3.10.13-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 10, "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "d1a777a0688bafd2a62050c680508769d9b6c14779f64fee591f4e135c11e711" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "57b83a4aa32bdbe7611f1290313ef24f2574dff5fa59181c0ccb26c14c688b73" }, - "cpython-3.10.13-linux-arm64-gnu": { + "cpython-3.10.13-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "arm64", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, "minor": 10, "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2927269de5d39b935285b676154793877102d6528a1302bab5d58c2cfbf848d9" - }, - "cpython-3.10.13-linux-i686-gnu": { - "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.10.13%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "08a3a1ff61b7ed2c87db7a9f88630781d98fabc2efb499f38ae0ead05973eb56" - }, - "cpython-3.10.13-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "0e2e96365d06411a78bc1e1b19cc5a148034743fe6ecf5a5c8e890985fcadbb4" - }, - "cpython-3.10.13-linux-ppc64le-gnu": { - "name": "cpython", - "arch": "ppc64le", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "24d5acc86495b15ba4b907d151ae62dfbcf2dbe61313b448f470aa333c8a9793" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "cab4c8756445d1d1987c7c94d3bcf323684e44fb9070329d8287d4c38e155711" }, "cpython-3.10.13-linux-s390x-gnu": { "name": "cpython", @@ -1393,19 +1041,30 @@ "major": 3, "minor": 10, "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "9dd2dc8f5bb6d011561a83577d4ca5d7ffcb0cd1aeecae32c8ba378eb77b2819" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "fe46914541126297c7a8636845c2e7188868eaa617bb6e293871fca4a5cb63f7" }, - "cpython-3.10.13-darwin-x86_64-none": { + "cpython-3.10.13-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 10, + "patch": 13, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.10.13%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "08a3a1ff61b7ed2c87db7a9f88630781d98fabc2efb499f38ae0ead05973eb56" + }, + "cpython-3.10.13-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 10, "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "b61f6f9cf0c35fd6df90b424e757a3bc1b483e8f8d8fadfa6c1ddd1a0c39c003" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "c8b99dcf267c574fdfbdf4e9d63ec7a4aa4608565fee3fba0b2f73843b9713b2" }, "cpython-3.10.13-linux-x86_64-gnu": { "name": "cpython", @@ -1415,8 +1074,8 @@ "major": 3, "minor": 10, "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fade5ea8fab973421e2e721d5ea4a6fa908db56e74b8af2bf3b4f4ce10b28aeb" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "41b20e9d87f57d27f608685b714a57eea81c9e079aa647d59837ec6659536626" }, "cpython-3.10.13-linux-x86_64-musl": { "name": "cpython", @@ -1426,8 +1085,19 @@ "major": 3, "minor": 10, "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64-unknown-linux-musl-lto-full.tar.zst", - "sha256": "95f66cf891eb474fb1904aa63e1e6f800238f7737269a21d933912cd26cbf816" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + "sha256": "fd18e6039be25bf23d13caf5140569df71d61312b823b715b3c788747fec48e9" + }, + "cpython-3.10.13-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 13, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "a41c1e28e2a646bac69e023873d40a43c5958d251c6adfa83d5811a7cb034c7a" }, "cpython-3.10.13-windows-x86_64-none": { "name": "cpython", @@ -1437,89 +1107,12 @@ "major": 3, "minor": 10, "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "8271db063eea7a32f327121b4d828bd10b9ecd1447d01fcfe8c7518e587ede63" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "6a2c8f37509556e5d463b1f437cdf7772ebd84cdf183c258d783e64bb3109505" }, - "cpython-3.10.13-linux-x86_64_v2-gnu": { + "cpython-3.10.12-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a13433753e7a4d28205df17c76e46481d7e74d9cfef61e2a10764903e5c11eb9" - }, - "cpython-3.10.13-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "6d020f3a2a61f9112e128364cec7d19c2aa2b26664f411ddc639712be552fd6b" - }, - "cpython-3.10.13-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e183e76dd20bd89a2f2510939681987fa3964003bdaee5091533b61d5bd8ec8d" - }, - "cpython-3.10.13-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "3adc05761f244f836993c0eca2312cf65d21b8a86103d73129a5cb9233ba1116" - }, - "cpython-3.10.13-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "bc4f0719037ed1fb84439fbf89c640859b841575f6804ef88dd969613a7c4980" - }, - "cpython-3.10.13-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.10.13%2B20240107-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "d29ee6b971179f13976559239e5741a3d7898360fd2591cc657a92ef1bfa4aa9" - }, - "cpython-3.10.12-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "a7d0cadbe867cc53dd47d7327244154157a7cca02edb88cf3bb760a4f91d4e44" - }, - "cpython-3.10.12-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -1528,31 +1121,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "e30f2b4fd9bd79b9122e2975f3c17c9ddd727f8326b2e246378e81f7ecc7d74f" }, - "cpython-3.10.12-linux-i686-gnu": { + "cpython-3.10.12-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "89c83fcdfd41c67e2dd2a037982556c657dc55fc1938c6f6cdcd5ffa614c1fb3" - }, - "cpython-3.10.12-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 10, "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "0743b9976f20b06d9cf12de9d1b2dfe06b13f76978275e9dac73a275624bde2c" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "a7d0cadbe867cc53dd47d7327244154157a7cca02edb88cf3bb760a4f91d4e44" }, - "cpython-3.10.12-linux-ppc64le-gnu": { + "cpython-3.10.12-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -1572,16 +1154,27 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-s390x-unknown-linux-gnu-debug-full.tar.zst", "sha256": "756579b52acb9b13b162ac901e56ff311def443e69d7f7259a91198b76a30ecb" }, - "cpython-3.10.12-darwin-x86_64-none": { + "cpython-3.10.12-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 10, + "patch": 12, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "89c83fcdfd41c67e2dd2a037982556c657dc55fc1938c6f6cdcd5ffa614c1fb3" + }, + "cpython-3.10.12-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 10, "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "f1fa448384dd48033825e56ee6b5afc76c5dd67dcf2b73b61d2b252ae2e87bca" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "0743b9976f20b06d9cf12de9d1b2dfe06b13f76978275e9dac73a275624bde2c" }, "cpython-3.10.12-linux-x86_64-gnu": { "name": "cpython", @@ -1605,6 +1198,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "b343cbe7c41b7698b568ea5252328cdccb213100efa71da8d3db6e21afd9f6cf" }, + "cpython-3.10.12-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 12, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "f1fa448384dd48033825e56ee6b5afc76c5dd67dcf2b73b61d2b252ae2e87bca" + }, "cpython-3.10.12-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -1616,86 +1220,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "cb6e7c84d9e369a0ee76c9ea73d415a113ba9982db58f44e6bab5414838d35f3" }, - "cpython-3.10.12-linux-x86_64_v2-gnu": { + "cpython-3.10.11-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b4d606147bcb75735dd55928330e111dec35fb2c825d2e3fd71eca23eaa11e5f" - }, - "cpython-3.10.12-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "78e9e5f03b4fd8ebd2253121f7ac8516d39761d28ac094d3f8025fc576ade1bd" - }, - "cpython-3.10.12-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3f85d12509db9cfe11785c0947d1e5baca0167e3eaa7065f9424a65fa159fb2f" - }, - "cpython-3.10.12-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "dbab7a72fa1ef45813cc25977dece74667a05a02b18dbc774aab7fcb395da424" - }, - "cpython-3.10.12-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "259e0c20061ed25a72e72912eb212a8571d0607659edba580272db809af7206e" - }, - "cpython-3.10.12-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "ce13736e1faa4fcb559c206c632f9232ba8f2b1f106c4af44433f3936b06d4d0" - }, - "cpython-3.10.11-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "da9c8a3cd04485fd397387ea2fa56f3cac71827aafb51d8438b2868f86eb345b" - }, - "cpython-3.10.11-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -1704,31 +1231,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "a5271cc014f2ce2ab54a0789556c15b84668e2afcc530512818c4b87c6a94483" }, - "cpython-3.10.11-linux-i686-gnu": { + "cpython-3.10.11-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "9304d6eeef48bd246a2959ebc76b20dbb2c6a81aa1d214f4471cb273c11717f2" - }, - "cpython-3.10.11-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 10, "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "60e76e136ab23b891ed1212e58bd11a73a19cd9fd884ec1c5653ca1c159d674e" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "da9c8a3cd04485fd397387ea2fa56f3cac71827aafb51d8438b2868f86eb345b" }, - "cpython-3.10.11-linux-ppc64le-gnu": { + "cpython-3.10.11-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -1737,16 +1253,27 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-ppc64le-unknown-linux-gnu-debug-full.tar.zst", "sha256": "ac32e3788109ff0cc536a6108072d9203217df744cf56d3a4ab0b19857d8e244" }, - "cpython-3.10.11-darwin-x86_64-none": { + "cpython-3.10.11-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 10, + "patch": 11, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "9304d6eeef48bd246a2959ebc76b20dbb2c6a81aa1d214f4471cb273c11717f2" + }, + "cpython-3.10.11-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 10, "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "e84c12aa0285235eed365971ceedf040f4d8014f5342d371e138a4da9e4e9b7c" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "60e76e136ab23b891ed1212e58bd11a73a19cd9fd884ec1c5653ca1c159d674e" }, "cpython-3.10.11-linux-x86_64-gnu": { "name": "cpython", @@ -1770,6 +1297,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "7918188e01a266915dd0945711e274d45c8d7fb540d48240e13c4fd96f43afbb" }, + "cpython-3.10.11-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 11, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "e84c12aa0285235eed365971ceedf040f4d8014f5342d371e138a4da9e4e9b7c" + }, "cpython-3.10.11-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -1781,86 +1319,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "9b4dc4a335b6122ce783bc80f5015b683e3ab1a56054751c5df494db0521da67" }, - "cpython-3.10.11-linux-x86_64_v2-gnu": { + "cpython-3.10.9-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "cd6316c2731d2282587475e781e240677c89a678694cf3e58f12e4c2e57add43" - }, - "cpython-3.10.11-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "af8c79e71dc8d958bcf80c675b383d210e08880bf80a08e038422274551f8ee5" - }, - "cpython-3.10.11-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ea5006c1545afed6f16e833a0d4bac14ec289cc0f5911e12a4ef8704a742cf8e" - }, - "cpython-3.10.11-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "532526149236ee3223af38e8946bff2d78ab7cb8756d91ed6676959a1eec4f01" - }, - "cpython-3.10.11-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "6ebe8edb6bb89109b351e6f4361be7f1563860e1ae1cb8926c17ca19588e5fa3" - }, - "cpython-3.10.11-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "58bd65a4e0b595b4d18dc9fc05dc384043d1106e6d0c221679eaefb277fa8784" - }, - "cpython-3.10.9-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "2508b8d4b725bb45c3e03d2ddd2b8441f1a74677cb6bd6076e692c0923135ded" - }, - "cpython-3.10.9-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -1869,9 +1330,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "2c0996dd1fe35314e06e042081b24fb53f3b7b361c3e1b94a6ed659c275ca069" }, - "cpython-3.10.9-linux-i686-gnu": { + "cpython-3.10.9-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 9, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "2508b8d4b725bb45c3e03d2ddd2b8441f1a74677cb6bd6076e692c0923135ded" + }, + "cpython-3.10.9-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -1880,9 +1352,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "f8c3a63620f412c4a9ccfb6e2435a96a55775550c81a452d164caa6d03a6a1da" }, - "cpython-3.10.9-windows-i686-none": { + "cpython-3.10.9-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -1891,17 +1363,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "3d79cfd229ec12b678bbfd79c30fb4cbad9950d6bfb29741d2315b11839998b4" }, - "cpython-3.10.9-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "1153b4d3b03cf1e1d8ec93c098160586f665fcc2d162c0812140a716a688df58" - }, "cpython-3.10.9-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -1924,6 +1385,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "1310f187a73b00164ec4ca34e643841c5c34cbb93fe0b3a3f9504e5ea5001ec7" }, + "cpython-3.10.9-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 9, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "1153b4d3b03cf1e1d8ec93c098160586f665fcc2d162c0812140a716a688df58" + }, "cpython-3.10.9-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -1935,86 +1407,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "4cfa6299a78a3959102c461d126e4869616f0a49c60b44220c000fc9aecddd78" }, - "cpython-3.10.9-linux-x86_64_v2-gnu": { + "cpython-3.10.8-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fbd196fc7680e499f374a6946b3618b42980e2890a6d533993337b563bd8abb0" - }, - "cpython-3.10.9-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "430a3d75bd4f0dca37bf00bab86c4f8fd78f09a37e5cbd89e07263c5fa88327a" - }, - "cpython-3.10.9-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ece59c02c9fdc77500bc1b60636077bc0a9536d6e63052c727db11488ae2de8c" - }, - "cpython-3.10.9-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "1aae9aa444aa03e03bdf24816c8c503fa76488b00fcd1e822350ef22a05edc00" - }, - "cpython-3.10.9-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "f4aab4df5c104f5d438a1a3b4601e45963a13c4100cbafee832336e47f6479cb" - }, - "cpython-3.10.9-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "5abe36719d4e42de95f5c6319785cfb024699beda1e72788231962d4e24c1e67" - }, - "cpython-3.10.8-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "f8ba5f87153a17717e900ff7bba20e2eefe8a53a5bd3c78f9f6922d6d910912d" - }, - "cpython-3.10.8-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -2023,9 +1418,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "879e76260be226512693e37a28cc3a6670b5ee270a4440e4b04a7b415dba451c" }, - "cpython-3.10.8-linux-i686-gnu": { + "cpython-3.10.8-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "f8ba5f87153a17717e900ff7bba20e2eefe8a53a5bd3c78f9f6922d6d910912d" + }, + "cpython-3.10.8-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -2034,9 +1440,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "ab434eccffeec4f6f51af017e4eed69d4f1ea55f48c5b89b8a8779df3fa799df" }, - "cpython-3.10.8-windows-i686-none": { + "cpython-3.10.8-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -2045,17 +1451,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "7547ea172f7fa3d7619855f28780da9feb615b6cb52c5c64d34f65b542799fee" }, - "cpython-3.10.8-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "a18f81ecc7da0779be960ad35c561a834866c0e6d1310a4f742fddfd6163753f" - }, "cpython-3.10.8-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -2078,6 +1473,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "bb87e933afcfd2e8de045e5a691feff1fb8fb06a09315b37d187762fddfc4546" }, + "cpython-3.10.8-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "a18f81ecc7da0779be960ad35c561a834866c0e6d1310a4f742fddfd6163753f" + }, "cpython-3.10.8-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -2089,86 +1495,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "ab40f9584be896c697c5fca351ab82d7b55f01b8eb0494f0a15a67562e49161a" }, - "cpython-3.10.8-linux-x86_64_v2-gnu": { + "cpython-3.10.7-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "96c8f51e23a1bd8b9a2d9adc0e2457fa74062f16d25d2d0d659b8a6c94631824" - }, - "cpython-3.10.8-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "c9a496301e22fabad5a27160fc199e25a10dc2775c26efa7b905ee9a58e5076a" - }, - "cpython-3.10.8-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "1053c4ca16bda71ae9f2a973cc82118b9e41aa6e426302d9709d9c74c60af50d" - }, - "cpython-3.10.8-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "bfcfb51a02082d1136259142b590a52a7b9f5e21bf1177bf988129b4f8f99933" - }, - "cpython-3.10.8-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "f23f744868d70cf2e9588aa961e9682060b6c3d6f55c65a61505e23c4895b7e4" - }, - "cpython-3.10.8-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "0574cab92283d240650aef5cc5716969e8e1a7a40d298d23bf0922d0988b19f9" - }, - "cpython-3.10.7-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "9f44cf63441a90f4ec99a032a2bda43971ae7964822daa0ee730a9cba15d50da" - }, - "cpython-3.10.7-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -2177,9 +1506,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "9f346729b523e860194635eb67c9f6bc8f12728ba7ddfe4fd80f2e6d685781e3" }, - "cpython-3.10.7-linux-i686-gnu": { + "cpython-3.10.7-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 7, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "9f44cf63441a90f4ec99a032a2bda43971ae7964822daa0ee730a9cba15d50da" + }, + "cpython-3.10.7-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -2188,9 +1528,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "a79816c50abeb2752530f68b4d7d95b6f48392f44a9a7f135b91807d76872972" }, - "cpython-3.10.7-windows-i686-none": { + "cpython-3.10.7-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -2199,17 +1539,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "323532701cb468199d6f14031b991f945d4bbf986ca818185e17e132d3763bdf" }, - "cpython-3.10.7-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "e03e28dc9fe55ea5ca06fece8f2f2a16646b217d28c0cd09ebcd512f444fdc90" - }, "cpython-3.10.7-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -2232,6 +1561,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "7f2c933d23c0f38cf145c2d6c65b5cf53bb589690d394fd4c01b2230c23c2bff" }, + "cpython-3.10.7-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 7, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "e03e28dc9fe55ea5ca06fece8f2f2a16646b217d28c0cd09ebcd512f444fdc90" + }, "cpython-3.10.7-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -2243,86 +1583,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "5363974e6ee6c91dbd6bc3533e38b02a26abc2ff1c9a095912f237b916be22d3" }, - "cpython-3.10.7-linux-x86_64_v2-gnu": { + "cpython-3.10.6-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fb44d19c8e4da5d5c467b6bbfc1b0350fa7faee71462fbd6ac3d69633c2b334a" - }, - "cpython-3.10.7-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "01258e2cd7a6950c31b850ae99ba7c4a351338ad3e01bab28b077d5eb7e1f509" - }, - "cpython-3.10.7-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c028a4c945329731be66285168a074ac2fc7f25d28c5bbb9bbbb532d45a82ab4" - }, - "cpython-3.10.7-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "ad93efc6c9b1bd25368b0e50e35ba6a21511d68ebfb209811585869666874cfc" - }, - "cpython-3.10.7-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b9ddf213ba0f69ac200dd50e5d2c6a52468307a584a64efe422ef537f446c0da" - }, - "cpython-3.10.7-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "9f3883a4eabe72243a6677467ec117247176353d12352f1dd1d32fa0ef92a18c" - }, - "cpython-3.10.6-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "159230851a69cf5cab80318bce48674244d7c6304de81f44c22ff0abdf895cfa" - }, - "cpython-3.10.6-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -2331,9 +1594,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "edc1c9742b824caebbc5cb224c8990aa8658b81593fd9219accf3efa3e849501" }, - "cpython-3.10.6-linux-i686-gnu": { + "cpython-3.10.6-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 6, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "159230851a69cf5cab80318bce48674244d7c6304de81f44c22ff0abdf895cfa" + }, + "cpython-3.10.6-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -2342,9 +1616,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "07fa4f5499b8885d1eea49caf5476d76305ab73494b7398dfd22c14093859e4f" }, - "cpython-3.10.6-windows-i686-none": { + "cpython-3.10.6-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -2353,17 +1627,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "8d9a259e15d5a1be48ef13cd5627d7f6c15eadf41a3539e99ed1deee668c075e" }, - "cpython-3.10.6-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "9405499573a7aa8b67d070d096ded4f3e571f18c2b34762606ecc8025290b122" - }, "cpython-3.10.6-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -2386,6 +1649,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "f859a72da0bb2f1261f8cebdac931b05b59474c7cb65cee8e85c34fc014dd452" }, + "cpython-3.10.6-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 6, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "9405499573a7aa8b67d070d096ded4f3e571f18c2b34762606ecc8025290b122" + }, "cpython-3.10.6-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -2397,86 +1671,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "01dc349721594b1bb5b582651f81479a24352f718fdf6279101caa0f377b160a" }, - "cpython-3.10.6-linux-x86_64_v2-gnu": { + "cpython-3.10.5-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "4a0cc9f5b57ca1eff38d0c9ee71f38f9bf28fbc34d150edd4914b32b69a84f17" - }, - "cpython-3.10.6-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "55d7d942223ca7ccb972dee02770ddd6b3cf61d674b3488f3917dac847688624" - }, - "cpython-3.10.6-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "38bc029b11deed33097aae7e1b2e56b6d85f06df24355ff6105bf85fbb2fcb9b" - }, - "cpython-3.10.6-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "da135320610e0e35926f2e5767b03d6c641220a4595423cbd3cf126fe1ff1868" - }, - "cpython-3.10.6-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "dc779272bd2363abda4a8a784f6dea2dd7d8b2d86cdcfae5e8970df6e5dfbd76" - }, - "cpython-3.10.6-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "8b3349fe4919adda35edce8108d96f398479620f01efdeee1ef1c667037f1c20" - }, - "cpython-3.10.5-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "f68d25dbe9daa96187fa9e05dd8969f46685547fecf1861a99af898f96a5379e" - }, - "cpython-3.10.5-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -2485,9 +1682,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "9fa6970a3d0a5dc26c4ed272bb1836d1f1f7a8f4b9d67f634d0262ff8c1fed0b" }, - "cpython-3.10.5-linux-i686-gnu": { + "cpython-3.10.5-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 5, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "f68d25dbe9daa96187fa9e05dd8969f46685547fecf1861a99af898f96a5379e" + }, + "cpython-3.10.5-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -2496,9 +1704,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "63fcfc425adabc034c851dadfb499de3083fd7758582191c12162ad2471256b0" }, - "cpython-3.10.5-windows-i686-none": { + "cpython-3.10.5-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -2507,17 +1715,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "e201192f0aa73904bc5a5f43d1ce4c9fb243dfe02138e690676713fe02c7d662" }, - "cpython-3.10.5-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "5e372e6738a733532aa985730d9a47ee4c77b7c706e91ef61d37aacbb2e54845" - }, "cpython-3.10.5-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -2540,6 +1737,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "3682e0add14a3bac654afe467a84981628b0c7ebdccd4ebf26dfaa916238e2fe" }, + "cpython-3.10.5-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 5, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "5e372e6738a733532aa985730d9a47ee4c77b7c706e91ef61d37aacbb2e54845" + }, "cpython-3.10.5-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -2551,86 +1759,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "cff35feefe423d4282e9a3e1bb756d0acbb2f776b1ada82c44c71ac3e1491448" }, - "cpython-3.10.5-linux-x86_64_v2-gnu": { + "cpython-3.10.4-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fdf3ade3f03f4a755f378d9abea1e9eb6a6a3fb18ce4620d5b7a6cb223a59741" - }, - "cpython-3.10.5-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "26ad08f1e22d75501fe8579795ab8aee3e52f9c1b1037e87dd349632fa4620b5" - }, - "cpython-3.10.5-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "137823acc0d98178c31ce0f75cef5fb0d3850edaf692ced2fab84ef9777d2c01" - }, - "cpython-3.10.5-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "125e101edfca4477694c12b3fa310ff81326cc51a1b1b889cbfc3577ff5b8f5d" - }, - "cpython-3.10.5-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "75fddbd0fa525d9455dab972c56ec8b6d39ddd62c12ab38dca81b558b1e70338" - }, - "cpython-3.10.5-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "ee4fdc2bc4797d64de3930750a8b0327cb789ee45483a23f09c913837171dd9b" - }, - "cpython-3.10.4-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "c404f226195d79933b1e0a3ec88f0b79d35c873de592e223e11008f3a37f83d6" - }, - "cpython-3.10.4-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -2639,9 +1770,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "092369e9d170c4c1074e1b305accb74f9486e6185d2e3f3f971869ff89538d3e" }, - "cpython-3.10.4-linux-i686-gnu": { + "cpython-3.10.4-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 4, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "c404f226195d79933b1e0a3ec88f0b79d35c873de592e223e11008f3a37f83d6" + }, + "cpython-3.10.4-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -2650,9 +1792,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "ba940a74a7434fe78d81aed9fb1e5ccdc3d97191a2db35716fc94e3b6604ace0" }, - "cpython-3.10.4-windows-i686-none": { + "cpython-3.10.4-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -2661,17 +1803,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "c37a47e46de93473916f700a790cb43515f00745fba6790004e2731ec934f4d3" }, - "cpython-3.10.4-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "e447f00fe53168d18cbfe110645dbf33982a17580b9e4424a411f9245d99cd21" - }, "cpython-3.10.4-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -2694,6 +1825,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "8b8b97f7746a3deca91ada408025457ced34f582dad2114b33ce6fec9cf35b28" }, + "cpython-3.10.4-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 4, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "e447f00fe53168d18cbfe110645dbf33982a17580b9e4424a411f9245d99cd21" + }, "cpython-3.10.4-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -2705,86 +1847,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "d636dc1bcca74dd9c6e3b26f7c081b3e229336e8378fe554bf8ba65fe780a2ac" }, - "cpython-3.10.4-linux-x86_64_v2-gnu": { + "cpython-3.10.3-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e0849600bee6c588f1aa5f4b0b9342292cfb6eb40de0c705f60bd71f22b713d0" - }, - "cpython-3.10.4-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "e036939d7b96fac35fcf9d01d2ddd6007f03f8a2e382d84ef31c0fcc37399e86" - }, - "cpython-3.10.4-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2875d103a83ff9f9f7ec2ec3ebc43d0e89f20416cd715b83b541b33f91d59832" - }, - "cpython-3.10.4-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "b83d649edc245c2f5f6347c09344108e9558dbbdf67ab7a596a76ff451394714" - }, - "cpython-3.10.4-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "839f9cb8f1b0e0eb347164a04323f832b945091ba3482724325d3eed1a75daab" - }, - "cpython-3.10.4-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "3c0c1be54a9711dde87f38d1d8fe97d6d05ca55d590870371b8302d0d4588463" - }, - "cpython-3.10.3-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "b1abefd0fc66922cf9749e4d5ceb97df4d3cfad0cd9cdc4bd04262a68d565698" - }, - "cpython-3.10.3-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -2793,9 +1858,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "101284d27578438da200be1f6b9a1ba621432c5549fa5517797ec320bf75e3d5" }, - "cpython-3.10.3-linux-i686-gnu": { + "cpython-3.10.3-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 3, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "b1abefd0fc66922cf9749e4d5ceb97df4d3cfad0cd9cdc4bd04262a68d565698" + }, + "cpython-3.10.3-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -2804,9 +1880,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "43c1cd6e203bfba1a2eeb96cd2a15ce0ebde0e72ecc9555934116459347a9c28" }, - "cpython-3.10.3-windows-i686-none": { + "cpython-3.10.3-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -2815,17 +1891,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "fbc0924a138937fe435fcdb20b0c6241290558e07f158e5578bd91cc8acef469" }, - "cpython-3.10.3-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "bc5d6f284b506104ff6b4e36cec84cbdb4602dfed4c6fe19971a808eb8c439ec" - }, "cpython-3.10.3-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -2848,6 +1913,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "7c034d8a5787744939335ce43d64f2ddcc830a74e63773408d0c8f3c3a4e7916" }, + "cpython-3.10.3-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 3, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "bc5d6f284b506104ff6b4e36cec84cbdb4602dfed4c6fe19971a808eb8c439ec" + }, "cpython-3.10.3-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -2859,86 +1935,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "72b91d26f54321ba90a86a3bbc711fa1ac31e0704fec352b36e70b0251ffb13c" }, - "cpython-3.10.3-linux-x86_64_v2-gnu": { + "cpython-3.10.2-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "65215230eacebebb87aa1e56d539fc1770870024dcfdadb5d0e18365c1b3b0a9" - }, - "cpython-3.10.3-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "87228dbf0063d26c67128e151f25f9de8cf5c2a041d5d424fef974cf32d5237f" - }, - "cpython-3.10.3-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e15dcd98efdde249e7c2857f80b93092245f8495d822cb6c7af3308a59729c21" - }, - "cpython-3.10.3-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "03897947f6920e12cd8fab4eca2c5c8d43ca2140f2eabf4a6d457ba8f41157f7" - }, - "cpython-3.10.3-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "412570cf01df665c848dacc213c645a3c17aa5045b1805a86c6d9fa2d82eb9ce" - }, - "cpython-3.10.3-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "7af8e1000ceb0a63bfdaa62fd064cb98fcf3712a5c8b71d17744f19307bfa89b" - }, - "cpython-3.10.2-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "1ef939fd471a9d346a7bc43d2c16fb483ddc4f98af6dad7f08a009e299977a1a" - }, - "cpython-3.10.2-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -2947,9 +1946,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "9936f1549f950311229465de509b35c062aa474e504c20a1d6f0f632da57e002" }, - "cpython-3.10.2-linux-i686-gnu": { + "cpython-3.10.2-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "1ef939fd471a9d346a7bc43d2c16fb483ddc4f98af6dad7f08a009e299977a1a" + }, + "cpython-3.10.2-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -2958,9 +1968,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "9be2a667f29ed048165cfb3f5dbe61703fd3e5956f8f517ae098740ac8411c0b" }, - "cpython-3.10.2-windows-i686-none": { + "cpython-3.10.2-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -2969,17 +1979,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "698b09b1b8321a4dc43d62f6230b62adcd0df018b2bcf5f1b4a7ce53dcf23bcc" }, - "cpython-3.10.2-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "bacf720c13ab67685a384f1417e9c2420972d88f29c8b7c26e72874177f2d120" - }, "cpython-3.10.2-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -3002,6 +2001,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "df246cf27db346081935d33ce0344a185d1f08b04a4500eb1e21d4d922ee7eb4" }, + "cpython-3.10.2-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "bacf720c13ab67685a384f1417e9c2420972d88f29c8b7c26e72874177f2d120" + }, "cpython-3.10.2-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -3013,86 +2023,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "7397e78a4fbe429144adc1f33af942bdd5175184e082ac88f3023b3a740dd1a0" }, - "cpython-3.10.2-linux-x86_64_v2-gnu": { + "cpython-3.10.0-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8c772010a27d7fd5a3e271c835c37b73bd41bc04a737523f12a3920bf721598b" - }, - "cpython-3.10.2-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "db2474e7a0b6fa37629f26308b3c5f0a018abae4bc9529ecc2fbf6ef5e741053" - }, - "cpython-3.10.2-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "03a6116ea6dbd5d4e667d4bec2b5032deefd604bc068908cd105327c417d0906" - }, - "cpython-3.10.2-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "d5489a850ffd2c5b85b4a52e26bb5022429e432823527c782e42c2f0dc333938" - }, - "cpython-3.10.2-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 10, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2262abca39027b52d96b397c5d3285b07a93fafd7dde52295876a331e9fb48c4" - }, - "cpython-3.10.2-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 10, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "b2956e7d7ed5dec84569e7381cfdbcd51dd1794f57af44864e3f2d8ee9f6a8c5" - }, - "cpython-3.10.0-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", - "sha256": null - }, - "cpython-3.10.0-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -3101,9 +2034,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-unknown-linux-gnu-debug-20211017T1616.tar.zst", "sha256": null }, - "cpython-3.10.0-linux-i686-gnu": { + "cpython-3.10.0-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 0, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", + "sha256": null + }, + "cpython-3.10.0-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -3112,9 +2056,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-i686-unknown-linux-gnu-debug-20211017T1616.tar.zst", "sha256": null }, - "cpython-3.10.0-windows-i686-none": { + "cpython-3.10.0-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -3123,17 +2067,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-i686-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", "sha256": null }, - "cpython-3.10.0-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 10, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", - "sha256": null - }, "cpython-3.10.0-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -3156,6 +2089,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-unknown-linux-musl-lto-20211017T1616.tar.zst", "sha256": null }, + "cpython-3.10.0-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 10, + "patch": 0, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", + "sha256": null + }, "cpython-3.10.0-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -3167,60 +2111,38 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", "sha256": null }, - "cpython-3.9.18-darwin-arm64-none": { + "cpython-3.9.18-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 9, + "patch": 18, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "d27efd4609a3e15ff901040529d5689be99f2ebfe5132ab980d066d775068265" + }, + "cpython-3.9.18-macos-aarch64-none": { + "name": "cpython", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 9, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "b7d31a15f7af359c59b01ed9c8accb4b6bdd1237b910699e6b2d14df8e2c1cdc" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "579f9b68bbb3a915cbab9682e4d3c253bc96b0556b8a860982c49c25c61f974a" }, - "cpython-3.9.18-linux-arm64-gnu": { + "cpython-3.9.18-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "arm64", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, "minor": 9, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "16a1ff546e24790bbea66a52d469aa57ef4090566b4cca6fee29528f59f28c40" - }, - "cpython-3.9.18-linux-i686-gnu": { - "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.9.18%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7faf8fdfbad04e0356a9d52c9b8be4d40ffef85c9ab3e312c45bd64997ef8aa9" - }, - "cpython-3.9.18-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "063c531d3c65f49212c9644ab0f00360a65d7c45bc1e6f78174685e2c165b260" - }, - "cpython-3.9.18-linux-ppc64le-gnu": { - "name": "cpython", - "arch": "ppc64le", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-ppc64le-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "bb8f31f18719654fd1f06724a4b37bb7a60abf8b6d14cec5fb1ba422bfa9bc31" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "c138eef19229351226a11e752230b8aa9d499ba9720f9f0574fa3260ccacb99b" }, "cpython-3.9.18-linux-s390x-gnu": { "name": "cpython", @@ -3230,19 +2152,30 @@ "major": 3, "minor": 9, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-s390x-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "70e4418ebabe2d61b8d09c9b9a9b2e00b7f6d8658b37de2a4ff9fe8c24bc2f1d" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-s390x-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "dd05eff699ce5a7eee545bc05e4869c4e64ee02bf0c70691bcee215604c6b393" }, - "cpython-3.9.18-darwin-x86_64-none": { + "cpython-3.9.18-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 9, + "patch": 18, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.9.18%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "7faf8fdfbad04e0356a9d52c9b8be4d40ffef85c9ab3e312c45bd64997ef8aa9" + }, + "cpython-3.9.18-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 9, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "aa2e549186ab9f831169ccc32965c81ba0fa62e471129f51988f40eaa9552309" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "212d413ab6f854f588cf368fdd2aa140bb7c7ee930e3f7ac1002cba1e50e9685" }, "cpython-3.9.18-linux-x86_64-gnu": { "name": "cpython", @@ -3252,8 +2185,8 @@ "major": 3, "minor": 9, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "93ff5a32b0874ea1eb888fb735663a64a26e5fac54079c64fdd48ac379da99d4" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "e1fa92798fab6f3b44a48f24b8e284660c34738d560681b206f0deb0616465f9" }, "cpython-3.9.18-linux-x86_64-musl": { "name": "cpython", @@ -3263,8 +2196,19 @@ "major": 3, "minor": 9, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64-unknown-linux-musl-lto-full.tar.zst", - "sha256": "ea096a98314f31186e1b0a6767d0a9b7b936a2d4003296887fd7e9fad0f50fd5" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + "sha256": "8bf88ae2100e609902d98ec775468e3a41a834f6528e632d6d971f5f75340336" + }, + "cpython-3.9.18-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 18, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "146537b9b4a1baa672eed94373e149ca1ee339c4df121e8916d8436265e5245e" }, "cpython-3.9.18-windows-x86_64-none": { "name": "cpython", @@ -3274,89 +2218,12 @@ "major": 3, "minor": 9, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "3b9c7d6ed94260b83ed8f44ee9a7b8fce392259ce6591e538601f7353061a884" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "924ed4f375ef73c73a725ef18ec6a72726456673d5a116f132f60860a25dd674" }, - "cpython-3.9.18-linux-x86_64_v2-gnu": { + "cpython-3.9.17-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "2520fa2853ddc81e43418b48217128a2a9d8b7e2a282b838ba7d3e66e2776f8d" - }, - "cpython-3.9.18-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "91e27451e1002ba29cf2e0e969654cf624ab72f3fe1c2adfd1a3aeaa1030ff0d" - }, - "cpython-3.9.18-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3ed47287cabecd02a074fad0ff8e2074ff39881e2c343e732cd1cdc62edefcbb" - }, - "cpython-3.9.18-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "20d61008ab8e1492a345521bf17f5b132d7d73d8ffd6f0dfb56d69506d2a6e73" - }, - "cpython-3.9.18-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "fca812213f43eeedc67d42d6589a209a4f921b26b11454a7de361a64c8c9187b" - }, - "cpython-3.9.18-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.9.18%2B20240107-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "bc8fbbb07f7bd22faee3e90886a9c46bc1393e695e93e67fb5cea61e4775048a" - }, - "cpython-3.9.17-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "2902e2a0add6d584999fa27896b721a359f7308404e936e80b01b07aa06e8f5e" - }, - "cpython-3.9.17-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -3365,31 +2232,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "1f6c43d92ba9f4e15149cf5db6ecde11e05eee92c070a085e44f46c559520257" }, - "cpython-3.9.17-linux-i686-gnu": { + "cpython-3.9.17-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "1a9b7edc16683410c27bc5b4b1761143bef7831a1ad172e7e3581c152c6837a2" - }, - "cpython-3.9.17-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 9, "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "ffac27bfb8bdf615d0fc6cbbe0becaa65b6ae73feec417919601497fce2be0ab" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "2902e2a0add6d584999fa27896b721a359f7308404e936e80b01b07aa06e8f5e" }, - "cpython-3.9.17-linux-ppc64le-gnu": { + "cpython-3.9.17-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -3409,16 +2265,27 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-s390x-unknown-linux-gnu-debug-full.tar.zst", "sha256": "bf8c846c1a4e52355d4ae294f4e1da9587d5415467eb6890bdf0f5a4c8cda396" }, - "cpython-3.9.17-darwin-x86_64-none": { + "cpython-3.9.17-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 9, + "patch": 17, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "1a9b7edc16683410c27bc5b4b1761143bef7831a1ad172e7e3581c152c6837a2" + }, + "cpython-3.9.17-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 9, "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "ba04f9813b78b61d60a27857949403a1b1dd8ac053e1f1aff72fe2689c238d3c" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "ffac27bfb8bdf615d0fc6cbbe0becaa65b6ae73feec417919601497fce2be0ab" }, "cpython-3.9.17-linux-x86_64-gnu": { "name": "cpython", @@ -3442,6 +2309,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "8496473a97e1dd43bf96fc1cf19f02f305608ef6a783e0112274e0ae01df4f2a" }, + "cpython-3.9.17-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 17, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "ba04f9813b78b61d60a27857949403a1b1dd8ac053e1f1aff72fe2689c238d3c" + }, "cpython-3.9.17-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -3453,86 +2331,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "209983b8227e4755197dfed4f6887e45b6a133f61e7eb913c0a934b0d0c3e00f" }, - "cpython-3.9.17-linux-x86_64_v2-gnu": { + "cpython-3.9.16-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "3faab5cb6c0bbb601be92575540036454fb34c0d7e4045760c62d782d8455f88" - }, - "cpython-3.9.17-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "64dbebbc6f9d5444156d641b17f1874f6f287f2d524435cfb9f1a580533146c7" - }, - "cpython-3.9.17-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c38db0bd66d15fd2f29bd6299dd74040a93fbe780c811827c3f3eca04f9aa3ca" - }, - "cpython-3.9.17-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "292f0c45e88866e4a8b122db5248c9d90f73e4d43af906eb97ff084d6e9d5d62" - }, - "cpython-3.9.17-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c9188a2b7edfb9e24d7cdfde239363ff8c79904901201041c0400d3f82923cb5" - }, - "cpython-3.9.17-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "3f397ca020c9aa46c792c20aa5a8e96f922a5e77d02b6f87413597b88efe7f51" - }, - "cpython-3.9.16-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "c86ed2bf3ff290af10f96183c53e2b29e954abb520806fbe01d3ef2f9d809a75" - }, - "cpython-3.9.16-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -3541,31 +2342,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "57ac7ce9d3dd32c1277ee7295daf5ad7b5ecc929e65b31f11b1e7b94cd355ed1" }, - "cpython-3.9.16-linux-i686-gnu": { + "cpython-3.9.16-macos-aarch64-none": { "name": "cpython", - "arch": "i686", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e2a0226165550492e895369ee1b69a515f82e12cb969656012ee8e1543409661" - }, - "cpython-3.9.16-windows-i686-none": { - "name": "cpython", - "arch": "i686", - "os": "windows", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 9, "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "d7994b5febb375bb131d028f98f4902ba308913c77095457ccd159b521e20c52" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "c86ed2bf3ff290af10f96183c53e2b29e954abb520806fbe01d3ef2f9d809a75" }, - "cpython-3.9.16-linux-ppc64le-gnu": { + "cpython-3.9.16-linux-powerpc64le-gnu": { "name": "cpython", - "arch": "ppc64le", + "arch": "powerpc64le", "os": "linux", "libc": "gnu", "major": 3, @@ -3574,16 +2364,27 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-ppc64le-unknown-linux-gnu-debug-full.tar.zst", "sha256": "8b2e7ddc6feb116dfa6829cfc478be90a374dc5ce123a98bc77e86d0e93e917d" }, - "cpython-3.9.16-darwin-x86_64-none": { + "cpython-3.9.16-linux-x86-gnu": { "name": "cpython", - "arch": "x86_64", - "os": "darwin", + "arch": "x86", + "os": "linux", + "libc": "gnu", + "major": 3, + "minor": 9, + "patch": 16, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "e2a0226165550492e895369ee1b69a515f82e12cb969656012ee8e1543409661" + }, + "cpython-3.9.16-windows-x86-none": { + "name": "cpython", + "arch": "x86", + "os": "windows", "libc": "none", "major": 3, "minor": 9, "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "5809626ca7907c8ea397341f3d5eafb280ed5b19cc5622e57b14d9b4362eba50" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "d7994b5febb375bb131d028f98f4902ba308913c77095457ccd159b521e20c52" }, "cpython-3.9.16-linux-x86_64-gnu": { "name": "cpython", @@ -3607,6 +2408,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "c397f292021b33531248ad8fede24ef6249cc6172347b2017f92b4a71845b8ed" }, + "cpython-3.9.16-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 16, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "5809626ca7907c8ea397341f3d5eafb280ed5b19cc5622e57b14d9b4362eba50" + }, "cpython-3.9.16-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -3618,86 +2430,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "199c821505e287c004c3796ba9ac4bd129d7793e1d833e9a7672ed03bdb397d4" }, - "cpython-3.9.16-linux-x86_64_v2-gnu": { + "cpython-3.9.15-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "35dcaff7a658b8c6bf5ffe1c4e2db95df5d647d28b4880a90f09bf3a70f42e8b" - }, - "cpython-3.9.16-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "73234f487b6be3c38a0f13200282d85866a248ed2ff8194b609dcf319af5e8d4" - }, - "cpython-3.9.16-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "4c00f35b9218e19065ba9e06c123505f056173a4f9b7eeb72ef768d0dfe8f867" - }, - "cpython-3.9.16-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "9bdf930d1b0ecd4b3e98a2b787aa25300ada62a42c20ac4d4c3bd923486e342b" - }, - "cpython-3.9.16-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b61ccbfd13415891a9120eaaa37ad5e3f6fac3bfad6123c951c955dbaafec6c3" - }, - "cpython-3.9.16-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "ffc2f51ce20eefaecc9d70daca4e73c051233006cd3d7a2f8dfca1189ed5584f" - }, - "cpython-3.9.15-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "1799b97619572ad595cd6d309bbcc57606138a57f4e90af04e04ee31d187e22f" - }, - "cpython-3.9.15-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -3706,9 +2441,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "0da1f081313b088c1381206e698e70fffdffc01e1b2ce284145c24ee5f5b4cbb" }, - "cpython-3.9.15-linux-i686-gnu": { + "cpython-3.9.15-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 15, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "1799b97619572ad595cd6d309bbcc57606138a57f4e90af04e04ee31d187e22f" + }, + "cpython-3.9.15-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -3717,9 +2463,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "cbc6a14835022d89f4ca6042a06c4959d74d4bbb58e70bdbe0fe8d2928934922" }, - "cpython-3.9.15-windows-i686-none": { + "cpython-3.9.15-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -3728,17 +2474,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "a5ad2a6ace97d458ad7b2857fba519c5c332362442d88e2b23ed818f243b8a78" }, - "cpython-3.9.15-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "50fd795eac55c4485e2fefbb8e7b365461817733c45becb50a7480a243e6000e" - }, "cpython-3.9.15-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -3761,6 +2496,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "4597f0009cfb52e748a57badab28edf84a263390b777c182b18c36d666a01440" }, + "cpython-3.9.15-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 15, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "50fd795eac55c4485e2fefbb8e7b365461817733c45becb50a7480a243e6000e" + }, "cpython-3.9.15-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -3772,86 +2518,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "d0f3ce1748a51779eedf155aea617c39426e3f7bfd93b4876cb172576b6e8bda" }, - "cpython-3.9.15-linux-x86_64_v2-gnu": { + "cpython-3.9.14-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "6222695583b32845fbbf17ec4a43981126ce0d6bb08b47a8c8853396e0f5e9f7" - }, - "cpython-3.9.15-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "440a4cffe00f2989628d1ff5ff0f2102998a328b3a0f605ea2fe110f93597598" - }, - "cpython-3.9.15-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "b5fbe25175f4338d6f5bb6f13ea6d714a4373a69b4d193c5037e8424ce5f53fb" - }, - "cpython-3.9.15-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "1289f63b4c3cc8a104e891b541a29642306c4bce7d0c82f571effbf52d5e2c50" - }, - "cpython-3.9.15-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "5bc44523a504d220e9af9acaa141ba9ac67e2fe998817cbca99b1bcc9a85d3cd" - }, - "cpython-3.9.15-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "77217f3033392b7bb17201d57b19259cd608d1f357d4f3ce2165e1a89bef1b14" - }, - "cpython-3.9.14-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "6b9d2ff724aff88a4d0790c86f2e5d17037736f35a796e71732624191ddd6e38" - }, - "cpython-3.9.14-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -3860,9 +2529,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "3020c743e4742d6e0e5d27fcb166c694bf1d9565369b2eaee9d68434304aebd2" }, - "cpython-3.9.14-linux-i686-gnu": { + "cpython-3.9.14-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 14, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "6b9d2ff724aff88a4d0790c86f2e5d17037736f35a796e71732624191ddd6e38" + }, + "cpython-3.9.14-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -3871,9 +2551,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "83a11c4f3d1c0ec39119bd0513a8684b59b68c3989cf1e5042d7417d4770c904" }, - "cpython-3.9.14-windows-i686-none": { + "cpython-3.9.14-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -3882,17 +2562,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "fae990eb312314102408cb0c0453dae670f0eb468f4cbf3e72327ceaa1276b46" }, - "cpython-3.9.14-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "186155e19b63da3248347415f888fbcf982c7587f6f927922ca243ae3f23ed2f" - }, "cpython-3.9.14-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -3915,6 +2584,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "5638c12d47eb81adf96615cea8a5a61e8414c3ac03a8b570d30ae9998cb6d030" }, + "cpython-3.9.14-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 14, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "186155e19b63da3248347415f888fbcf982c7587f6f927922ca243ae3f23ed2f" + }, "cpython-3.9.14-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -3926,86 +2606,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "49f27a3a18b4c2d765b0656c6529378a20b3e37fdb0aca9490576ff7a67243a9" }, - "cpython-3.9.14-linux-x86_64_v2-gnu": { + "cpython-3.9.13-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a78cb80ca372ea98aca28d33a220b3fe91761c6dadaf92f646b82f3b569822c1" - }, - "cpython-3.9.14-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "d20c9ff5f3474a80948cc4533a799573e5e3c62cd0477224f5e1ec1da656c6be" - }, - "cpython-3.9.14-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "53d3c637264a57317b304a88ddd418463fe0b37b874495e3ae55940b5c62b363" - }, - "cpython-3.9.14-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "3ad497929e6e14596a18f4dfd09d48fb4bbefff23f06e5ca5b4d9f79ac31f646" - }, - "cpython-3.9.14-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "92e5a9fd569c0b1c161958e2e1d3198cb4e27b524d02cf3008b260dba658961f" - }, - "cpython-3.9.14-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "35681efee0142c0873cbf27d5e54eddb13e61439a563b358a02fed9909011d0e" - }, - "cpython-3.9.13-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "8612e9328663c0747d1eae36b218d11c2fbc53c39ec7512c7ad6b1b57374a5dc" - }, - "cpython-3.9.13-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -4014,9 +2617,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "8c706ebb2c8970da4fbec95b0520b4632309bc6a3e115cf309e38f181b553d14" }, - "cpython-3.9.13-linux-i686-gnu": { + "cpython-3.9.13-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 13, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "8612e9328663c0747d1eae36b218d11c2fbc53c39ec7512c7ad6b1b57374a5dc" + }, + "cpython-3.9.13-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -4025,9 +2639,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "7d33637b48c45acf8805d5460895dca29bf2740fd2cf502fde6c6a00637db6b5" }, - "cpython-3.9.13-windows-i686-none": { + "cpython-3.9.13-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4036,17 +2650,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "3860abee418825c6a33f76fe88773fb05eb4bc724d246f1af063106d9ea3f999" }, - "cpython-3.9.13-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "16d21a6e62c19c574a4a225961e80966449095a8eb2c4150905e30d4e807cf86" - }, "cpython-3.9.13-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4069,6 +2672,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "c7e48545a8291fe1be909c4454b5c48df0ee4e69e2b5e13b6144b4199c31f895" }, + "cpython-3.9.13-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 13, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "16d21a6e62c19c574a4a225961e80966449095a8eb2c4150905e30d4e807cf86" + }, "cpython-3.9.13-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -4080,86 +2694,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "6ef2b164cae483c61da30fb6d245762b8d6d91346d66cb421989d6d1462e5a48" }, - "cpython-3.9.13-linux-x86_64_v2-gnu": { + "cpython-3.9.12-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "37ace8159aa2f5157abea2c0d854434f21e6676b90eeaf58d313778f64540ca7" - }, - "cpython-3.9.13-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "0648910f5f0341f192da23a6e58aedbfdbd0ee17d41207fcaf25c957f295dfa7" - }, - "cpython-3.9.13-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "59a24171f794139b41d47b3e734c82d02bc11956a4c092a3d28d94a5d0ab7f54" - }, - "cpython-3.9.13-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "b6f756062f9df0791e0148f0d37c62c3fd13b49e8fc8b18805dcac3a010c67ef" - }, - "cpython-3.9.13-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "920fd94d4352495a804d4ee725764ae7499b29c5f1c15d11cc4380a873498dbf" - }, - "cpython-3.9.13-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "e2a41aab57dc2c0f61c02702510722456fb06947b620fef0e5ad2c649d4701fa" - }, - "cpython-3.9.12-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "b3d09b3c12295e893ee8f2cb60e8af94d8a21fc5c65016282925220f5270b85b" - }, - "cpython-3.9.12-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -4168,9 +2705,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "202ef64e43570f0843ff5895fd9c1a2c36a96b48d52842fa95842d7d11025b20" }, - "cpython-3.9.12-linux-i686-gnu": { + "cpython-3.9.12-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 12, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "b3d09b3c12295e893ee8f2cb60e8af94d8a21fc5c65016282925220f5270b85b" + }, + "cpython-3.9.12-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -4179,9 +2727,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "e52fdbe61dea847323cd6e81142d16a571dca9c0bcde3bfe5ae75a8d3d1a3bf4" }, - "cpython-3.9.12-windows-i686-none": { + "cpython-3.9.12-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4190,17 +2738,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "361b8fa66d6b5d5623fd5e64af29cf220a693ba86d031bf7ce2b61e1ea50f568" }, - "cpython-3.9.12-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "825970ae30ae7a30a5b039aa25f1b965e2d1fe046e196e61fa2a3af8fef8c5d9" - }, "cpython-3.9.12-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4223,6 +2760,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "eb122ab2bf0b2d71926984bc7cf5fef65b415abfe01a0974ed6c1a2502fac764" }, + "cpython-3.9.12-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 12, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "825970ae30ae7a30a5b039aa25f1b965e2d1fe046e196e61fa2a3af8fef8c5d9" + }, "cpython-3.9.12-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -4234,86 +2782,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "c49f8b07e9c4dcfd7a5b55c131e882a4ebdf9f37fef1c7820c3ce9eb23bab8ab" }, - "cpython-3.9.12-linux-x86_64_v2-gnu": { + "cpython-3.9.11-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "53dadc107ce8d1ba5f94ca87edbeef485c8a07b2c0e942a99a7c7e163fafb6f4" - }, - "cpython-3.9.12-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "347357fa079cdc17b6becd521e64911661c6263e64f161721cf0db39466ee4c3" - }, - "cpython-3.9.12-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "e7ba8a790a3194a0847ef0b09b8dd8542b9c2ca689b2f4ee4ecb771a44ea41f2" - }, - "cpython-3.9.12-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "06011e3ea1f3265c72a15d133a76a3d308377228ac6392206348312e76c321d9" - }, - "cpython-3.9.12-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "afdd26e418ab5a2e5e10b42eb98c79bafd7c198363f70aad8628266b7f3532ab" - }, - "cpython-3.9.12-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "281509ccab077369f69cdd88c6d82488dfa65a626350cc7290ca13954f305b4c" - }, - "cpython-3.9.11-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "6d9f20607a20e2cc5ad1428f7366832dc68403fc15f2e4f195817187e7b6dbbf" - }, - "cpython-3.9.11-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -4322,9 +2793,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "e1f3ae07a28a687f8602fb4d29a1b72cc5e113c61dc6769d0d85081ab3e09c71" }, - "cpython-3.9.11-linux-i686-gnu": { + "cpython-3.9.11-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 11, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "6d9f20607a20e2cc5ad1428f7366832dc68403fc15f2e4f195817187e7b6dbbf" + }, + "cpython-3.9.11-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -4333,9 +2815,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "0be0a5f524c68d521be2417565ca43f3125b1845f996d6d62266aa431e673f93" }, - "cpython-3.9.11-windows-i686-none": { + "cpython-3.9.11-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4344,17 +2826,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "f06338422e7e3ad25d0cd61864bdb36d565d46440dd363cbb98821d388ed377a" }, - "cpython-3.9.11-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "35e649618e7e602778e72b91c9c50c97d01a0c3509d16225a1f41dd0fd6575f0" - }, "cpython-3.9.11-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4377,6 +2848,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "d83eb5c897120e32287cb6fe5c24dd2dcae00878b3f9d7002590d468bd5de0f1" }, + "cpython-3.9.11-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 11, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "35e649618e7e602778e72b91c9c50c97d01a0c3509d16225a1f41dd0fd6575f0" + }, "cpython-3.9.11-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -4388,86 +2870,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "1fe3c519d43737dc7743aec43f72735e1429c79e06e3901b21bad67b642f1a10" }, - "cpython-3.9.11-linux-x86_64_v2-gnu": { + "cpython-3.9.10-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "7d7a745a3d6eade5fd7352b444c4c91ce562a2d188716d6995155c2ed5d1e337" - }, - "cpython-3.9.11-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "6ce18f9b6808aece6e45f2859cd12e0420c2a3372b9a9130503d8f5689d68f89" - }, - "cpython-3.9.11-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ea43a4a473ebf99af4f7b9ae1d00e3bd6c5a2a329096e5657590ed5d823903dd" - }, - "cpython-3.9.11-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "48452b551065ae3040967cc47c36fe7eb42ea4f8be07c3c19a47ceab9a5957ea" - }, - "cpython-3.9.11-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "a1e7c4554d200d4fa50dc43cda6db020df99f75bb105dd7c44139c44d5040fae" - }, - "cpython-3.9.11-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "395a2c3b64446af61100ccf8fdce7dfa3cff91fd72ca0b0a1147a5aac5f7aa9c" - }, - "cpython-3.9.10-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "ba1b63600ed8d9f3b8d739657bd8e7f5ca167de29a1a58d04b2cd9940b289464" - }, - "cpython-3.9.10-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -4476,9 +2881,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "8bf7ac2cd5825b8fde0a6e535266a57c97e82fd5a97877940920b403ca5e53d7" }, - "cpython-3.9.10-linux-i686-gnu": { + "cpython-3.9.10-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 10, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "ba1b63600ed8d9f3b8d739657bd8e7f5ca167de29a1a58d04b2cd9940b289464" + }, + "cpython-3.9.10-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -4487,9 +2903,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "3e3bf4d3e71a2131e6c064d1e5019f58cb9c58fdceae4b76b26ac978a6d49aad" }, - "cpython-3.9.10-windows-i686-none": { + "cpython-3.9.10-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4498,17 +2914,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "7f3ca15f89775f76a32e6ea9b2c9778ebf0cde753c5973d4493959e75dd92488" }, - "cpython-3.9.10-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "ef2f090ff920708b4b9aa5d6adf0dc930c09a4bf638d71e6883091f9e629193d" - }, "cpython-3.9.10-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4531,6 +2936,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "2744b817f249c0563b844cddd5aba4cc2fd449489b8bd59980d7a31de3a4ece1" }, + "cpython-3.9.10-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 10, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "ef2f090ff920708b4b9aa5d6adf0dc930c09a4bf638d71e6883091f9e629193d" + }, "cpython-3.9.10-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -4542,86 +2958,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "56b2738599131d03b39b914ea0597862fd9096e5e64816bf19466bf026e74f0c" }, - "cpython-3.9.10-linux-x86_64_v2-gnu": { + "cpython-3.9.7-linux-aarch64-gnu": { "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64_v2-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8317251d7480b9fc8b4f0b76dbb91b339bc55760cc92715e66f91a3055396497" - }, - "cpython-3.9.10-linux-x86_64_v2-musl": { - "name": "cpython", - "arch": "x86_64_v2", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64_v2-unknown-linux-musl-lto-full.tar.zst", - "sha256": "8aca72895482f0730ac20f829fd47ce004bb0f52fc99fdd500cf22a2b1288d9a" - }, - "cpython-3.9.10-linux-x86_64_v3-gnu": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64_v3-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "c2a8ef2fd0f2c033371e931c2588444c5872b3c1fe7c787e0139e9ee03177ea6" - }, - "cpython-3.9.10-linux-x86_64_v3-musl": { - "name": "cpython", - "arch": "x86_64_v3", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64_v3-unknown-linux-musl-lto-full.tar.zst", - "sha256": "c6d9c35ac9e3553eb1f56a955c24487f1910c58827efee846b38ee8dbfef6390" - }, - "cpython-3.9.10-linux-x86_64_v4-gnu": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "gnu", - "major": 3, - "minor": 9, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64_v4-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "162da56c07f3bec783c3c202b274c89ab115dae82ab7783b04404c7e315b819f" - }, - "cpython-3.9.10-linux-x86_64_v4-musl": { - "name": "cpython", - "arch": "x86_64_v4", - "os": "linux", - "libc": "musl", - "major": 3, - "minor": 9, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64_v4-unknown-linux-musl-lto-full.tar.zst", - "sha256": "e9c51af9f3e684349a8b2f3422849520d64e1a6b8c3546367fe0e239c2ff7399" - }, - "cpython-3.9.7-darwin-arm64-none": { - "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", - "sha256": null - }, - "cpython-3.9.7-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -4630,9 +2969,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-aarch64-unknown-linux-gnu-debug-20211017T1616.tar.zst", "sha256": null }, - "cpython-3.9.7-linux-i686-gnu": { + "cpython-3.9.7-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 7, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", + "sha256": null + }, + "cpython-3.9.7-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -4641,9 +2991,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-i686-unknown-linux-gnu-debug-20211017T1616.tar.zst", "sha256": null }, - "cpython-3.9.7-windows-i686-none": { + "cpython-3.9.7-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4652,17 +3002,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-i686-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", "sha256": null }, - "cpython-3.9.7-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", - "sha256": null - }, "cpython-3.9.7-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4685,6 +3024,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-unknown-linux-musl-lto-20211017T1616.tar.zst", "sha256": null }, + "cpython-3.9.7-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 7, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", + "sha256": null + }, "cpython-3.9.7-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -4696,20 +3046,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", "sha256": null }, - "cpython-3.9.6-darwin-arm64-none": { + "cpython-3.9.6-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-aarch64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", - "sha256": null - }, - "cpython-3.9.6-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -4718,9 +3057,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-aarch64-unknown-linux-gnu-debug-20210724T1424.tar.zst", "sha256": null }, - "cpython-3.9.6-linux-i686-gnu": { + "cpython-3.9.6-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 6, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-aarch64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", + "sha256": null + }, + "cpython-3.9.6-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -4729,9 +3079,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-i686-unknown-linux-gnu-debug-20210724T1424.tar.zst", "sha256": null }, - "cpython-3.9.6-windows-i686-none": { + "cpython-3.9.6-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4740,17 +3090,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-i686-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", "sha256": null }, - "cpython-3.9.6-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", - "sha256": null - }, "cpython-3.9.6-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4773,6 +3112,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-unknown-linux-musl-lto-20210724T1424.tar.zst", "sha256": null }, + "cpython-3.9.6-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 6, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", + "sha256": null + }, "cpython-3.9.6-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -4784,10 +3134,10 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", "sha256": null }, - "cpython-3.9.5-darwin-arm64-none": { + "cpython-3.9.5-macos-aarch64-none": { "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 9, @@ -4795,9 +3145,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-aarch64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", "sha256": null }, - "cpython-3.9.5-linux-i686-gnu": { + "cpython-3.9.5-linux-x86-gnu": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -4806,9 +3156,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-i686-unknown-linux-gnu-debug-20210506T0943.tar.zst", "sha256": null }, - "cpython-3.9.5-windows-i686-none": { + "cpython-3.9.5-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4817,17 +3167,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-i686-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", "sha256": null }, - "cpython-3.9.5-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", - "sha256": null - }, "cpython-3.9.5-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4850,6 +3189,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-unknown-linux-musl-lto-20210506T0943.tar.zst", "sha256": null }, + "cpython-3.9.5-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 5, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", + "sha256": null + }, "cpython-3.9.5-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -4861,10 +3211,10 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", "sha256": null }, - "cpython-3.9.4-darwin-arm64-none": { + "cpython-3.9.4-macos-aarch64-none": { "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 9, @@ -4872,9 +3222,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-aarch64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", "sha256": null }, - "cpython-3.9.4-linux-i686-gnu": { + "cpython-3.9.4-linux-x86-gnu": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -4883,9 +3233,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-i686-unknown-linux-gnu-debug-20210414T1515.tar.zst", "sha256": null }, - "cpython-3.9.4-windows-i686-none": { + "cpython-3.9.4-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4894,17 +3244,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-i686-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", "sha256": null }, - "cpython-3.9.4-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", - "sha256": null - }, "cpython-3.9.4-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4927,6 +3266,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-unknown-linux-musl-lto-20210414T1515.tar.zst", "sha256": null }, + "cpython-3.9.4-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 4, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", + "sha256": null + }, "cpython-3.9.4-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -4938,10 +3288,10 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", "sha256": null }, - "cpython-3.9.3-darwin-arm64-none": { + "cpython-3.9.3-macos-aarch64-none": { "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 9, @@ -4949,9 +3299,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-aarch64-apple-darwin-pgo%2Blto-20210413T2055.tar.zst", "sha256": null }, - "cpython-3.9.3-windows-i686-none": { + "cpython-3.9.3-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -4960,17 +3310,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-i686-pc-windows-msvc-shared-pgo-20210413T2055.tar.zst", "sha256": null }, - "cpython-3.9.3-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-apple-darwin-pgo%2Blto-20210413T2055.tar.zst", - "sha256": null - }, "cpython-3.9.3-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -4993,6 +3332,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-unknown-linux-musl-lto-20210413T2055.tar.zst", "sha256": null }, + "cpython-3.9.3-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 3, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-apple-darwin-pgo%2Blto-20210413T2055.tar.zst", + "sha256": null + }, "cpython-3.9.3-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5004,10 +3354,10 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-pc-windows-msvc-shared-pgo-20210413T2055.tar.zst", "sha256": null }, - "cpython-3.9.2-darwin-arm64-none": { + "cpython-3.9.2-macos-aarch64-none": { "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 9, @@ -5015,9 +3365,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-aarch64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", "sha256": null }, - "cpython-3.9.2-linux-i686-gnu": { + "cpython-3.9.2-linux-x86-gnu": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5026,9 +3376,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-i686-unknown-linux-gnu-debug-20210327T1202.tar.zst", "sha256": null }, - "cpython-3.9.2-windows-i686-none": { + "cpython-3.9.2-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5037,17 +3387,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-i686-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", "sha256": null }, - "cpython-3.9.2-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", - "sha256": null - }, "cpython-3.9.2-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5070,6 +3409,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-unknown-linux-musl-lto-20210327T1202.tar.zst", "sha256": null }, + "cpython-3.9.2-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", + "sha256": null + }, "cpython-3.9.2-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5081,9 +3431,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", "sha256": null }, - "cpython-3.9.1-windows-i686-none": { + "cpython-3.9.1-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5092,17 +3442,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-i686-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", "sha256": null }, - "cpython-3.9.1-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 1, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-apple-darwin-pgo-20210103T1125.tar.zst", - "sha256": null - }, "cpython-3.9.1-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5125,6 +3464,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-unknown-linux-musl-debug-20210103T1125.tar.zst", "sha256": null }, + "cpython-3.9.1-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 1, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-apple-darwin-pgo-20210103T1125.tar.zst", + "sha256": null + }, "cpython-3.9.1-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5136,9 +3486,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", "sha256": null }, - "cpython-3.9.0-windows-i686-none": { + "cpython-3.9.0-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5147,17 +3497,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-i686-pc-windows-msvc-shared-pgo-20201021T0245.tar.zst", "sha256": null }, - "cpython-3.9.0-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 9, - "patch": 0, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-apple-darwin-pgo-20201020T0626.tar.zst", - "sha256": null - }, "cpython-3.9.0-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5180,6 +3519,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-unknown-linux-musl-debug-20201020T0627.tar.zst", "sha256": null }, + "cpython-3.9.0-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 9, + "patch": 0, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-apple-darwin-pgo-20201020T0626.tar.zst", + "sha256": null + }, "cpython-3.9.0-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5191,49 +3541,38 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-pc-windows-msvc-shared-pgo-20201021T0245.tar.zst", "sha256": null }, - "cpython-3.8.18-darwin-arm64-none": { + "cpython-3.8.18-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.8.18%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "f426349265897fb3715f19f474f45e17406d77701eb1b60953f9b32e51c779b9" - }, - "cpython-3.8.18-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, "minor": 8, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.8.18%2B20240107-aarch64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "8aeb623f50866c9ee0260471a664e048b31836ce8793490895d2f7b5b5792e84" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "6d71175a090950c2063680f250b8799ab39eb139aa1721c853d8950aadd1d4e2" }, - "cpython-3.8.18-windows-i686-none": { + "cpython-3.8.18-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 18, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "c732c068cddcd6a008c1d6d8e35802f5bdc7323bd2eb64e77210d3d5fe4740c2" + }, + "cpython-3.8.18-windows-x86-none": { + "name": "cpython", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, "minor": 8, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.8.18%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "875983fccf91310805164528150adfde1ac63564d0c3967d48086c6fdb9f568b" - }, - "cpython-3.8.18-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.8.18%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "bfcd4a61998e105a78dbac2b68f1f264cd7bedc5ef11f89ec10911f23b445616" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "9f94c7b54b97116cd308e73cda0b7a7b7fff4515932c5cbba18eeae9ec798351" }, "cpython-3.8.18-linux-x86_64-gnu": { "name": "cpython", @@ -5243,8 +3582,8 @@ "major": 3, "minor": 8, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.8.18%2B20240107-x86_64-unknown-linux-gnu-debug-full.tar.zst", - "sha256": "ff02848c574ccc581d21433f20eef333faf06f4fcd35bf2c6264553bec3f1643" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + "sha256": "189ae3b8249c57217e3253f9fc89857e088763cf2107a3f22ab2ac2398f41a65" }, "cpython-3.8.18-linux-x86_64-musl": { "name": "cpython", @@ -5254,8 +3593,19 @@ "major": 3, "minor": 8, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.8.18%2B20240107-x86_64-unknown-linux-musl-lto-full.tar.zst", - "sha256": "a0f8f26137b9971bfa7fc657b55362dd23a69d9df6d39e35bdae211834350f62" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + "sha256": "fa1bf64cf52d830e7b4bba486c447ee955af644d167df7c42afd169c5dc71d6a" + }, + "cpython-3.8.18-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 18, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "4d4b65dd821ce13dcf6dfea3ad5c2d4c3d3a8c2b7dd49fc35c1d79f66238e89b" }, "cpython-3.8.18-windows-x86_64-none": { "name": "cpython", @@ -5265,23 +3615,12 @@ "major": 3, "minor": 8, "patch": 18, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.8.18%2B20240107-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", - "sha256": "0675bf51ad66c149c311e8da4a358b0e0fc28801770163d8053d9aadf6bdb556" + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + "sha256": "c63abd9365a13196eb9f65db864f95b85c1f90b770d218c1acd104e6b48a99d3" }, - "cpython-3.8.17-darwin-arm64-none": { + "cpython-3.8.17-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "d08a542bed35fc74ac6e8f6884c8aa29a77ff2f4ed04a06dcf91578dea622f9a" - }, - "cpython-3.8.17-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -5290,9 +3629,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "eaee5a0b79cc28943e19df54f314634795aee43a6670ce99c0306893a18fa784" }, - "cpython-3.8.17-linux-i686-gnu": { + "cpython-3.8.17-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 17, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "d08a542bed35fc74ac6e8f6884c8aa29a77ff2f4ed04a06dcf91578dea622f9a" + }, + "cpython-3.8.17-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5301,9 +3651,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "61ac08680c022f180a32dc82d84548aeb92c7194a489e3b3c532dc48f999d757" }, - "cpython-3.8.17-windows-i686-none": { + "cpython-3.8.17-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5312,17 +3662,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "0931d8ca0e060c6ac1dfcf6bb9b6dea0ac3a9d95daf7906a88128045f4464bf8" }, - "cpython-3.8.17-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 17, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "2c4925f5cf37d498e0d8cfe7b10591cc5f0cd80d2582f566b12006e6f96958b1" - }, "cpython-3.8.17-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5345,6 +3684,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "a316ba0b1f425b04c8dfd7a8a18a05d72ae5852732d401b16d7439bdf25caec3" }, + "cpython-3.8.17-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 17, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "2c4925f5cf37d498e0d8cfe7b10591cc5f0cd80d2582f566b12006e6f96958b1" + }, "cpython-3.8.17-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5356,20 +3706,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "68c7d03de5283c4812f2706c797b2139999a28cec647bc662d1459a922059318" }, - "cpython-3.8.16-darwin-arm64-none": { + "cpython-3.8.16-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "bfc91d0a1d6d6dfaa5a31c925aa6adae82bd1ae5eb17813a9f0a50bf9d3e6305" - }, - "cpython-3.8.16-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -5378,9 +3717,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "423d43d93e2fe33b41ad66d35426f16541f09fee9d7272ae5decf5474ebbc225" }, - "cpython-3.8.16-linux-i686-gnu": { + "cpython-3.8.16-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 16, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "bfc91d0a1d6d6dfaa5a31c925aa6adae82bd1ae5eb17813a9f0a50bf9d3e6305" + }, + "cpython-3.8.16-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5389,9 +3739,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "9aa3e559130a47c33ee2b67f6ca69e2f10d8f70c1fd1e2871763b892372a6d9e" }, - "cpython-3.8.16-windows-i686-none": { + "cpython-3.8.16-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5400,17 +3750,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "5de953621402c11cc7db65ba15d45779e838d7ce78e7aa8d43c7d78fff177f13" }, - "cpython-3.8.16-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 16, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "21c0f4a0fa6ee518b9f2f1901c9667e3baf45d9f84235408b7ca50499d19f56d" - }, "cpython-3.8.16-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5433,6 +3772,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "f7d46196b44d12a26209ac74061200aac478b96c253eea93a0b9734efa642779" }, + "cpython-3.8.16-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 16, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "21c0f4a0fa6ee518b9f2f1901c9667e3baf45d9f84235408b7ca50499d19f56d" + }, "cpython-3.8.16-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5444,20 +3794,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "6316713c2dcb30127b38ced249fa9608830a33459580b71275a935aaa8cd5d5f" }, - "cpython-3.8.15-darwin-arm64-none": { + "cpython-3.8.15-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "fc0f944e6f01ed649f79c873af1c317db61d2136b82081b4d7cbb7755f878035" - }, - "cpython-3.8.15-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -5466,9 +3805,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "2e80025eda686c14a9a0618ced40043c1d577a754b904fd7a382cd41abf9ca00" }, - "cpython-3.8.15-linux-i686-gnu": { + "cpython-3.8.15-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 15, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "fc0f944e6f01ed649f79c873af1c317db61d2136b82081b4d7cbb7755f878035" + }, + "cpython-3.8.15-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5477,9 +3827,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "b8436415ea9bd9970fb766f791a14b0e14ce6351fc4604eb158f1425e8bb4a33" }, - "cpython-3.8.15-windows-i686-none": { + "cpython-3.8.15-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5488,17 +3838,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "98bb2315c3567316c30b060d613c8d6067b368b64f08ef8fe6196341637c1d78" }, - "cpython-3.8.15-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 15, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "e4fd2fa2255295fbdcfadb8b48014fa80810305eccb246d355880aabb45cbe93" - }, "cpython-3.8.15-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5521,6 +3860,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "231b35d3c2cff0372d17cea7ff5168c0684a920b94a912ffc965c2518cacb694" }, + "cpython-3.8.15-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 15, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "e4fd2fa2255295fbdcfadb8b48014fa80810305eccb246d355880aabb45cbe93" + }, "cpython-3.8.15-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5532,20 +3882,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "59beac5610e6da0848ebaccd72f91f6aaaeed65ef59606d006af909e9e79beba" }, - "cpython-3.8.14-darwin-arm64-none": { + "cpython-3.8.14-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "d17a3fcc161345efa2ec0b4ab9c9ed6c139d29128f2e34bb636338a484aa7b72" - }, - "cpython-3.8.14-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -5554,9 +3893,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "a14d8b5cbd8e1ca45cbcb49f4bf0b0440dc86eb95b7c3da3c463a704a3b4593c" }, - "cpython-3.8.14-linux-i686-gnu": { + "cpython-3.8.14-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 14, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "d17a3fcc161345efa2ec0b4ab9c9ed6c139d29128f2e34bb636338a484aa7b72" + }, + "cpython-3.8.14-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5565,9 +3915,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "631bb90fe8f2965d03400b268de90fe155ce51961296360d6578b7151aa9ef4c" }, - "cpython-3.8.14-windows-i686-none": { + "cpython-3.8.14-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5576,17 +3926,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "e43f7a5044eac91e95df59fd08bf96f13245898876fc2afd90a081cfcd847e35" }, - "cpython-3.8.14-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 14, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "62edfea77b42e87ca2d85c482319211cd2dd68d55ba85c99f1834f7b64a60133" - }, "cpython-3.8.14-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5609,6 +3948,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "c6da442aaea160179a9379b297ccb3ba09b825fc27d84577fc28e62911451e7d" }, + "cpython-3.8.14-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 14, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "62edfea77b42e87ca2d85c482319211cd2dd68d55ba85c99f1834f7b64a60133" + }, "cpython-3.8.14-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5620,20 +3970,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "6986b3e6edf7b37f96ea940b7ccba7b767ed3ea9b3faec2a2a60e5b2c4443314" }, - "cpython-3.8.13-darwin-arm64-none": { + "cpython-3.8.13-linux-aarch64-gnu": { "name": "cpython", - "arch": "arm64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "a204e5f9e1566bdc170b163300a29fc9580d5c65cd6e896caf6500cd64471373" - }, - "cpython-3.8.13-linux-arm64-gnu": { - "name": "cpython", - "arch": "arm64", + "arch": "aarch64", "os": "linux", "libc": "gnu", "major": 3, @@ -5642,9 +3981,20 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-aarch64-unknown-linux-gnu-debug-full.tar.zst", "sha256": "3a927205db4686c182b5e8f3fc7fd7d82ec8f61c70d5b2bfddd9673c7ddc07ba" }, - "cpython-3.8.13-linux-i686-gnu": { + "cpython-3.8.13-macos-aarch64-none": { "name": "cpython", - "arch": "i686", + "arch": "aarch64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 13, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "a204e5f9e1566bdc170b163300a29fc9580d5c65cd6e896caf6500cd64471373" + }, + "cpython-3.8.13-linux-x86-gnu": { + "name": "cpython", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5653,9 +4003,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "6daf0405beae6d127a2dcae61d51a719236b861b4cabc220727e48547fd6f045" }, - "cpython-3.8.13-windows-i686-none": { + "cpython-3.8.13-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5664,17 +4014,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "5630739d1c6fcfbf90311d236c5e46314fc4b439364429bee12d0ffc95e134fb" }, - "cpython-3.8.13-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 13, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "f706a62de8582bf84b8b693c993314cd786f3e78639892cfd9a7283a526696f9" - }, "cpython-3.8.13-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5697,6 +4036,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "410f3223021d1b439cf8e4da699f868adada2066e354d88a00b5f365dc66c4bf" }, + "cpython-3.8.13-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 13, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "f706a62de8582bf84b8b693c993314cd786f3e78639892cfd9a7283a526696f9" + }, "cpython-3.8.13-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5708,10 +4058,10 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "c36b703b8b806a047ba71e5e85734ac78d204d3a2b7ebc2efcdc7d4af6f6c263" }, - "cpython-3.8.12-darwin-arm64-none": { + "cpython-3.8.12-macos-aarch64-none": { "name": "cpython", - "arch": "arm64", - "os": "darwin", + "arch": "aarch64", + "os": "macos", "libc": "none", "major": 3, "minor": 8, @@ -5719,9 +4069,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", "sha256": "386f667f8d49b6c34aee1910cdc0b5b41883f9406f98e7d59a3753990b1cdbac" }, - "cpython-3.8.12-linux-i686-gnu": { + "cpython-3.8.12-linux-x86-gnu": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5730,9 +4080,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-i686-unknown-linux-gnu-debug-full.tar.zst", "sha256": "7cfac9a57e262be3e889036d7fc570293e6d3d74411ee23e1fa9aa470d387e6a" }, - "cpython-3.8.12-windows-i686-none": { + "cpython-3.8.12-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5741,17 +4091,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-i686-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "3e2e6c7de78b1924aad37904fed7bfbac6efa2bef05348e9be92180b2f2b1ae1" }, - "cpython-3.8.12-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 12, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", - "sha256": "cf614d96e2001d526061b3ce0569c79057fd0074ace472ff4f5f601262e08cdb" - }, "cpython-3.8.12-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5774,6 +4113,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-unknown-linux-musl-lto-full.tar.zst", "sha256": "3d958e3f984637d8ca4a90a2e068737b268f87fc615121a6f1808cd46ccacc48" }, + "cpython-3.8.12-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 12, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + "sha256": "cf614d96e2001d526061b3ce0569c79057fd0074ace472ff4f5f601262e08cdb" + }, "cpython-3.8.12-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5785,9 +4135,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", "sha256": "33f278416ba8074f2ca6d7f8c17b311b60537c9e6431fd47948784c2a78ea227" }, - "cpython-3.8.11-linux-i686-gnu": { + "cpython-3.8.11-linux-x86-gnu": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5796,9 +4146,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-i686-unknown-linux-gnu-debug-20210724T1424.tar.zst", "sha256": null }, - "cpython-3.8.11-windows-i686-none": { + "cpython-3.8.11-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5807,17 +4157,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-i686-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", "sha256": null }, - "cpython-3.8.11-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 11, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", - "sha256": null - }, "cpython-3.8.11-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5840,6 +4179,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-unknown-linux-musl-lto-20210724T1424.tar.zst", "sha256": null }, + "cpython-3.8.11-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 11, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", + "sha256": null + }, "cpython-3.8.11-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5851,9 +4201,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", "sha256": null }, - "cpython-3.8.10-linux-i686-gnu": { + "cpython-3.8.10-linux-x86-gnu": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5862,9 +4212,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-i686-unknown-linux-gnu-debug-20210506T0943.tar.zst", "sha256": null }, - "cpython-3.8.10-windows-i686-none": { + "cpython-3.8.10-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5873,17 +4223,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-i686-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", "sha256": null }, - "cpython-3.8.10-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 10, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", - "sha256": null - }, "cpython-3.8.10-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5906,6 +4245,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-unknown-linux-musl-lto-20210506T0943.tar.zst", "sha256": null }, + "cpython-3.8.10-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 10, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", + "sha256": null + }, "cpython-3.8.10-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5917,9 +4267,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", "sha256": null }, - "cpython-3.8.9-linux-i686-gnu": { + "cpython-3.8.9-linux-x86-gnu": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5928,9 +4278,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-i686-unknown-linux-gnu-debug-20210414T1515.tar.zst", "sha256": null }, - "cpython-3.8.9-windows-i686-none": { + "cpython-3.8.9-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -5939,17 +4289,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-i686-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", "sha256": null }, - "cpython-3.8.9-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", - "sha256": null - }, "cpython-3.8.9-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -5972,6 +4311,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-unknown-linux-musl-lto-20210414T1515.tar.zst", "sha256": null }, + "cpython-3.8.9-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 9, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", + "sha256": null + }, "cpython-3.8.9-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -5983,9 +4333,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", "sha256": null }, - "cpython-3.8.8-linux-i686-gnu": { + "cpython-3.8.8-linux-x86-gnu": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "linux", "libc": "gnu", "major": 3, @@ -5994,9 +4344,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-i686-unknown-linux-gnu-debug-20210327T1202.tar.zst", "sha256": null }, - "cpython-3.8.8-windows-i686-none": { + "cpython-3.8.8-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6005,17 +4355,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-i686-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", "sha256": null }, - "cpython-3.8.8-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 8, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", - "sha256": null - }, "cpython-3.8.8-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6038,6 +4377,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-unknown-linux-musl-lto-20210327T1202.tar.zst", "sha256": null }, + "cpython-3.8.8-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 8, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", + "sha256": null + }, "cpython-3.8.8-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6049,9 +4399,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", "sha256": null }, - "cpython-3.8.7-windows-i686-none": { + "cpython-3.8.7-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6060,17 +4410,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-i686-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", "sha256": null }, - "cpython-3.8.7-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-apple-darwin-pgo-20210103T1125.tar.zst", - "sha256": null - }, "cpython-3.8.7-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6093,6 +4432,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-unknown-linux-musl-debug-20210103T1125.tar.zst", "sha256": null }, + "cpython-3.8.7-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 7, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-apple-darwin-pgo-20210103T1125.tar.zst", + "sha256": null + }, "cpython-3.8.7-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6104,9 +4454,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", "sha256": null }, - "cpython-3.8.6-windows-i686-none": { + "cpython-3.8.6-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6115,17 +4465,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-i686-pc-windows-msvc-shared-pgo-20201021T0233.tar.zst", "sha256": null }, - "cpython-3.8.6-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-apple-darwin-pgo-20201020T0626.tar.zst", - "sha256": null - }, "cpython-3.8.6-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6148,6 +4487,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-unknown-linux-musl-debug-20201020T0627.tar.zst", "sha256": null }, + "cpython-3.8.6-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 6, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-apple-darwin-pgo-20201020T0626.tar.zst", + "sha256": null + }, "cpython-3.8.6-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6159,9 +4509,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-pc-windows-msvc-shared-pgo-20201021T0232.tar.zst", "sha256": null }, - "cpython-3.8.5-windows-i686-none": { + "cpython-3.8.5-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6170,17 +4520,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200830/cpython-3.8.5-i686-pc-windows-msvc-shared-pgo-20200830T2311.tar.zst", "sha256": null }, - "cpython-3.8.5-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.8.5-x86_64-apple-darwin-pgo-20200823T2228.tar.zst", - "sha256": null - }, "cpython-3.8.5-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6203,6 +4542,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.8.5-x86_64-unknown-linux-musl-debug-20200823T0036.tar.zst", "sha256": null }, + "cpython-3.8.5-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 5, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.8.5-x86_64-apple-darwin-pgo-20200823T2228.tar.zst", + "sha256": null + }, "cpython-3.8.5-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6214,9 +4564,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200830/cpython-3.8.5-x86_64-pc-windows-msvc-shared-pgo-20200830T2254.tar.zst", "sha256": null }, - "cpython-3.8.3-windows-i686-none": { + "cpython-3.8.3-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6225,17 +4575,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-i686-pc-windows-msvc-shared-pgo-20200518T0154.tar.zst", "sha256": null }, - "cpython-3.8.3-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200530/cpython-3.8.3-x86_64-apple-darwin-pgo-20200530T1845.tar.zst", - "sha256": null - }, "cpython-3.8.3-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6258,6 +4597,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-x86_64-unknown-linux-musl-debug-20200518T0040.tar.zst", "sha256": null }, + "cpython-3.8.3-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 3, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200530/cpython-3.8.3-x86_64-apple-darwin-pgo-20200530T1845.tar.zst", + "sha256": null + }, "cpython-3.8.3-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6269,9 +4619,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-x86_64-pc-windows-msvc-shared-pgo-20200517T2207.tar.zst", "sha256": null }, - "cpython-3.8.2-windows-i686-none": { + "cpython-3.8.2-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6280,17 +4630,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-i686-pc-windows-msvc-shared-pgo-20200418T2315.tar.zst", "sha256": null }, - "cpython-3.8.2-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 8, - "patch": 2, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-apple-darwin-pgo-20200418T2238.tar.zst", - "sha256": null - }, "cpython-3.8.2-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6302,6 +4641,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-unknown-linux-gnu-debug-20200418T2305.tar.zst", "sha256": null }, + "cpython-3.8.2-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 8, + "patch": 2, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-apple-darwin-pgo-20200418T2238.tar.zst", + "sha256": null + }, "cpython-3.8.2-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6313,9 +4663,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-pc-windows-msvc-shared-pgo-20200418T2315.tar.zst", "sha256": null }, - "cpython-3.7.9-windows-i686-none": { + "cpython-3.7.9-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6324,17 +4674,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-i686-pc-windows-msvc-shared-pgo-20200823T0159.tar.zst", "sha256": null }, - "cpython-3.7.9-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 7, - "patch": 9, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.7.9-x86_64-apple-darwin-pgo-20200823T2228.tar.zst", - "sha256": null - }, "cpython-3.7.9-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6357,6 +4696,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-x86_64-unknown-linux-musl-debug-20200823T0036.tar.zst", "sha256": null }, + "cpython-3.7.9-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 7, + "patch": 9, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.7.9-x86_64-apple-darwin-pgo-20200823T2228.tar.zst", + "sha256": null + }, "cpython-3.7.9-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6368,9 +4718,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-x86_64-pc-windows-msvc-shared-pgo-20200823T0118.tar.zst", "sha256": null }, - "cpython-3.7.7-windows-i686-none": { + "cpython-3.7.7-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6379,28 +4729,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-i686-pc-windows-msvc-shared-pgo-20200517T2153.tar.zst", "sha256": null }, - "cpython-3.7.7-shared-windows-none": { - "name": "cpython", - "arch": "windows", - "os": "shared", - "libc": "none", - "major": 3, - "minor": 7, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200408/cpython-3.7.7-windows-amd64-shared-20200409T0108.tar.zst", - "sha256": null - }, - "cpython-3.7.7-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 7, - "patch": 7, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200530/cpython-3.7.7-x86_64-apple-darwin-pgo-20200530T1845.tar.zst", - "sha256": null - }, "cpython-3.7.7-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6423,6 +4751,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-x86_64-unknown-linux-musl-debug-20200518T0040.tar.zst", "sha256": null }, + "cpython-3.7.7-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 7, + "patch": 7, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200530/cpython-3.7.7-x86_64-apple-darwin-pgo-20200530T1845.tar.zst", + "sha256": null + }, "cpython-3.7.7-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6434,9 +4773,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-x86_64-pc-windows-msvc-shared-pgo-20200517T2128.tar.zst", "sha256": null }, - "cpython-3.7.6-windows-i686-none": { + "cpython-3.7.6-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6445,28 +4784,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-windows-x86-shared-pgo-20200217T0110.tar.zst", "sha256": null }, - "cpython-3.7.6-shared-windows-none": { - "name": "cpython", - "arch": "windows", - "os": "shared", - "libc": "none", - "major": 3, - "minor": 7, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-windows-amd64-shared-20200216T2324.tar.zst", - "sha256": null - }, - "cpython-3.7.6-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 7, - "patch": 6, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-macos-20200216T2344.tar.zst", - "sha256": null - }, "cpython-3.7.6-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6489,6 +4806,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200217/cpython-3.7.6-linux64-musl-20200218T0557.tar.zst", "sha256": null }, + "cpython-3.7.6-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 7, + "patch": 6, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-macos-20200216T2344.tar.zst", + "sha256": null + }, "cpython-3.7.6-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6500,9 +4828,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-windows-amd64-shared-pgo-20200217T0022.tar.zst", "sha256": null }, - "cpython-3.7.5-windows-i686-none": { + "cpython-3.7.5-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6511,17 +4839,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-windows-x86-20191025T0549.tar.zst", "sha256": null }, - "cpython-3.7.5-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 7, - "patch": 5, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-macos-20191026T0535.tar.zst", - "sha256": null - }, "cpython-3.7.5-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6544,6 +4861,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-linux64-musl-20191026T0603.tar.zst", "sha256": null }, + "cpython-3.7.5-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 7, + "patch": 5, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-macos-20191026T0535.tar.zst", + "sha256": null + }, "cpython-3.7.5-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6555,9 +4883,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-windows-amd64-20191025T0540.tar.zst", "sha256": null }, - "cpython-3.7.4-windows-i686-none": { + "cpython-3.7.4-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6566,17 +4894,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-windows-x86-20190817T0235.tar.zst", "sha256": null }, - "cpython-3.7.4-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 7, - "patch": 4, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-macos-20190817T0220.tar.zst", - "sha256": null - }, "cpython-3.7.4-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6599,6 +4916,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-linux64-musl-20190817T0227.tar.zst", "sha256": null }, + "cpython-3.7.4-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 7, + "patch": 4, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-macos-20190817T0220.tar.zst", + "sha256": null + }, "cpython-3.7.4-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", @@ -6610,9 +4938,9 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-windows-amd64-20190817T0227.tar.zst", "sha256": null }, - "cpython-3.7.3-windows-i686-none": { + "cpython-3.7.3-windows-x86-none": { "name": "cpython", - "arch": "i686", + "arch": "x86", "os": "windows", "libc": "none", "major": 3, @@ -6621,17 +4949,6 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-windows-x86-20190709T0348.tar.zst", "sha256": null }, - "cpython-3.7.3-darwin-x86_64-none": { - "name": "cpython", - "arch": "x86_64", - "os": "darwin", - "libc": "none", - "major": 3, - "minor": 7, - "patch": 3, - "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-macos-20190618T0523.tar.zst", - "sha256": null - }, "cpython-3.7.3-linux-x86_64-gnu": { "name": "cpython", "arch": "x86_64", @@ -6654,6 +4971,17 @@ "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-linux64-musl-20190618T0400.tar.zst", "sha256": null }, + "cpython-3.7.3-macos-x86_64-none": { + "name": "cpython", + "arch": "x86_64", + "os": "macos", + "libc": "none", + "major": 3, + "minor": 7, + "patch": 3, + "url": "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-macos-20190618T0523.tar.zst", + "sha256": null + }, "cpython-3.7.3-windows-x86_64-none": { "name": "cpython", "arch": "x86_64", diff --git a/crates/uv-toolchain/src/downloads.rs b/crates/uv-toolchain/src/downloads.rs new file mode 100644 index 000000000..ae1adfe59 --- /dev/null +++ b/crates/uv-toolchain/src/downloads.rs @@ -0,0 +1,438 @@ +use std::fmt::{self, Display}; +use std::io; +use std::path::{Path, PathBuf}; +use std::str::FromStr; + +use crate::PythonVersion; +use thiserror::Error; +use uv_client::BetterReqwestError; + +use futures::TryStreamExt; + +use tokio_util::compat::FuturesAsyncReadCompatExt; +use tracing::debug; +use url::Url; +use uv_fs::Simplified; + +#[derive(Error, Debug)] +pub enum Error { + #[error("operating system not supported: {0}")] + OsNotSupported(String), + #[error("architecture not supported: {0}")] + ArchNotSupported(String), + #[error("libc type could not be detected")] + LibcNotDetected(), + #[error("invalid python version: {0}")] + InvalidPythonVersion(String), + #[error("download failed")] + NetworkError(#[from] BetterReqwestError), + #[error("download failed")] + NetworkMiddlewareError(#[source] anyhow::Error), + #[error(transparent)] + ExtractError(#[from] uv_extract::Error), + #[error("invalid download url")] + InvalidUrl(#[from] url::ParseError), + #[error("failed to create download directory")] + DownloadDirError(#[source] io::Error), + #[error("failed to copy to: {0}", to.user_display())] + CopyError { + to: PathBuf, + #[source] + err: io::Error, + }, + #[error("failed to read toolchain directory: {0}", dir.user_display())] + ReadError { + dir: PathBuf, + #[source] + err: io::Error, + }, +} + +#[derive(Debug, PartialEq)] +pub struct PythonDownload { + key: &'static str, + implementation: ImplementationName, + arch: Arch, + os: Os, + libc: Libc, + major: u8, + minor: u8, + patch: u8, + url: &'static str, + sha256: Option<&'static str>, +} + +#[derive(Debug)] +pub struct PythonDownloadRequest { + version: Option, + implementation: Option, + arch: Option, + os: Option, + libc: Option, +} + +impl PythonDownloadRequest { + pub fn new( + version: Option, + implementation: Option, + arch: Option, + os: Option, + libc: Option, + ) -> Self { + Self { + version, + implementation, + arch, + os, + libc, + } + } + + #[must_use] + pub fn with_implementation(mut self, implementation: ImplementationName) -> Self { + self.implementation = Some(implementation); + self + } + + #[must_use] + pub fn with_arch(mut self, arch: Arch) -> Self { + self.arch = Some(arch); + self + } + + #[must_use] + pub fn with_os(mut self, os: Os) -> Self { + self.os = Some(os); + self + } + + #[must_use] + pub fn with_libc(mut self, libc: Libc) -> Self { + self.libc = Some(libc); + self + } + + pub fn fill(mut self) -> Result { + if self.implementation.is_none() { + self.implementation = Some(ImplementationName::Cpython); + } + if self.arch.is_none() { + self.arch = Some(Arch::from_env()?); + } + if self.os.is_none() { + self.os = Some(Os::from_env()?); + } + if self.libc.is_none() { + self.libc = Some(Libc::from_env()?); + } + Ok(self) + } +} + +impl FromStr for PythonDownloadRequest { + type Err = Error; + + fn from_str(s: &str) -> Result { + // TOOD(zanieb): Implement parsing of additional request parts + let version = PythonVersion::from_str(s).map_err(Error::InvalidPythonVersion)?; + Ok(Self::new(Some(version), None, None, None, None)) + } +} + +#[derive(Debug, PartialEq)] +pub enum Libc { + Gnu, + Musl, + None, +} + +#[derive(Debug, PartialEq)] +pub enum ImplementationName { + Cpython, +} +#[derive(Debug, PartialEq)] +pub struct Platform { + os: Os, + arch: Arch, + libc: Libc, +} + +include!("python_versions.inc"); + +pub enum DownloadResult { + AlreadyAvailable(PathBuf), + Fetched(PathBuf), +} + +impl PythonDownload { + /// Return the [`PythonDownload`] corresponding to the key, if it exists. + pub fn from_key(key: &str) -> Option<&PythonDownload> { + PYTHON_DOWNLOADS.iter().find(|&value| value.key == key) + } + + pub fn from_request(request: &PythonDownloadRequest) -> Option<&'static PythonDownload> { + for download in PYTHON_DOWNLOADS { + if let Some(arch) = &request.arch { + if download.arch != *arch { + continue; + } + } + if let Some(os) = &request.os { + if download.os != *os { + continue; + } + } + if let Some(implementation) = &request.implementation { + if download.implementation != *implementation { + continue; + } + } + if let Some(version) = &request.version { + if download.major != version.major() { + continue; + } + if download.minor != version.minor() { + continue; + } + if let Some(patch) = version.patch() { + if download.patch != patch { + continue; + } + } + } + return Some(download); + } + None + } + + pub fn url(&self) -> &str { + self.url + } + + pub fn sha256(&self) -> Option<&str> { + self.sha256 + } + + /// Download and extract + pub async fn fetch( + &self, + client: &uv_client::BaseClient, + path: &Path, + ) -> Result { + let url = Url::parse(self.url)?; + let path = path.join(self.key).clone(); + + // If it already exists, return it + if path.is_dir() { + return Ok(DownloadResult::AlreadyAvailable(path)); + } + + let filename = url.path_segments().unwrap().last().unwrap(); + let response = client.get(url.clone()).send().await?; + + // Ensure the request was successful. + response.error_for_status_ref()?; + + // Download and extract into a temporary directory. + let temp_dir = tempfile::tempdir().map_err(Error::DownloadDirError)?; + + debug!( + "Downloading {url} to temporary location {}", + temp_dir.path().display() + ); + let reader = response + .bytes_stream() + .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) + .into_async_read(); + + debug!("Extracting {filename}"); + uv_extract::stream::archive(reader.compat(), filename, temp_dir.path()).await?; + + // Extract the top-level directory. + let extracted = match uv_extract::strip_component(temp_dir.path()) { + Ok(top_level) => top_level, + Err(uv_extract::Error::NonSingularArchive(_)) => temp_dir.into_path(), + Err(err) => return Err(err.into()), + }; + + // Persist it to the target + debug!("Moving {} to {}", extracted.display(), path.user_display()); + fs_err::tokio::rename(extracted, &path) + .await + .map_err(|err| Error::CopyError { + to: path.clone(), + err, + })?; + + Ok(DownloadResult::Fetched(path)) + } + + pub fn python_version(&self) -> PythonVersion { + PythonVersion::from_str(&format!("{}.{}.{}", self.major, self.minor, self.patch)) + .expect("Python downloads should always have valid versions") + } +} + +impl Platform { + pub fn new(os: Os, arch: Arch, libc: Libc) -> Self { + Self { os, arch, libc } + } + pub fn from_env() -> Result { + Ok(Self::new( + Os::from_env()?, + Arch::from_env()?, + Libc::from_env()?, + )) + } +} + +/// All supported operating systems. +#[derive(Debug, Clone, Eq, PartialEq)] +pub enum Os { + Windows, + Linux, + Macos, + FreeBsd, + NetBsd, + OpenBsd, + Dragonfly, + Illumos, + Haiku, +} + +impl fmt::Display for Os { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::Windows => write!(f, "Windows"), + Self::Macos => write!(f, "MacOS"), + Self::FreeBsd => write!(f, "FreeBSD"), + Self::NetBsd => write!(f, "NetBSD"), + Self::Linux => write!(f, "Linux"), + Self::OpenBsd => write!(f, "OpenBSD"), + Self::Dragonfly => write!(f, "DragonFly"), + Self::Illumos => write!(f, "Illumos"), + Self::Haiku => write!(f, "Haiku"), + } + } +} + +impl Os { + pub(crate) fn from_env() -> Result { + Self::from_str(std::env::consts::OS) + } +} + +impl FromStr for Os { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "windows" => Ok(Self::Windows), + "linux" => Ok(Self::Linux), + "macos" => Ok(Self::Macos), + "freebsd" => Ok(Self::FreeBsd), + "netbsd" => Ok(Self::NetBsd), + "openbsd" => Ok(Self::OpenBsd), + "dragonfly" => Ok(Self::Dragonfly), + "illumos" => Ok(Self::Illumos), + "haiku" => Ok(Self::Haiku), + _ => Err(Error::OsNotSupported(s.to_string())), + } + } +} + +/// All supported CPU architectures +#[derive(Debug, Clone, Copy, Eq, PartialEq)] +pub enum Arch { + Aarch64, + Armv6L, + Armv7L, + Powerpc64Le, + Powerpc64, + X86, + X86_64, + S390X, +} + +impl fmt::Display for Arch { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match *self { + Self::Aarch64 => write!(f, "aarch64"), + Self::Armv6L => write!(f, "armv6l"), + Self::Armv7L => write!(f, "armv7l"), + Self::Powerpc64Le => write!(f, "ppc64le"), + Self::Powerpc64 => write!(f, "ppc64"), + Self::X86 => write!(f, "i686"), + Self::X86_64 => write!(f, "x86_64"), + Self::S390X => write!(f, "s390x"), + } + } +} + +impl FromStr for Arch { + type Err = Error; + + fn from_str(s: &str) -> Result { + match s.to_lowercase().as_str() { + "aarch64" | "arm64" => Ok(Self::Aarch64), + "armv6l" => Ok(Self::Armv6L), + "armv7l" => Ok(Self::Armv7L), + "powerpc64le" | "ppc64le" => Ok(Self::Powerpc64Le), + "powerpc64" | "ppc64" => Ok(Self::Powerpc64), + "x86" | "i686" | "i386" => Ok(Self::X86), + "x86_64" | "amd64" => Ok(Self::X86_64), + "s390x" => Ok(Self::S390X), + _ => Err(Error::ArchNotSupported(s.to_string())), + } + } +} + +impl Arch { + pub(crate) fn from_env() -> Result { + Self::from_str(std::env::consts::ARCH) + } +} + +impl Libc { + pub(crate) fn from_env() -> Result { + // TODO(zanieb): Perform this lookup + match std::env::consts::OS { + "linux" => Ok(Libc::Gnu), + "windows" | "macos" => Ok(Libc::None), + _ => Err(Error::LibcNotDetected()), + } + } +} + +impl fmt::Display for Libc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Libc::Gnu => f.write_str("gnu"), + Libc::None => f.write_str("none"), + Libc::Musl => f.write_str("musl"), + } + } +} + +impl From for Error { + fn from(error: reqwest::Error) -> Self { + Self::NetworkError(BetterReqwestError::from(error)) + } +} + +impl From for Error { + fn from(error: reqwest_middleware::Error) -> Self { + match error { + reqwest_middleware::Error::Middleware(error) => Self::NetworkMiddlewareError(error), + reqwest_middleware::Error::Reqwest(error) => { + Self::NetworkError(BetterReqwestError::from(error)) + } + } + } +} + +impl Display for PythonDownload { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(self.key) + } +} diff --git a/crates/uv-toolchain/src/find.rs b/crates/uv-toolchain/src/find.rs new file mode 100644 index 000000000..69022bae6 --- /dev/null +++ b/crates/uv-toolchain/src/find.rs @@ -0,0 +1,110 @@ +use std::collections::BTreeSet; +use std::ffi::OsStr; +use std::path::{Path, PathBuf}; + +use crate::downloads::{Arch, Error, Libc, Os}; +use crate::python_version::PythonVersion; + +use once_cell::sync::Lazy; + +/// The directory where Python toolchains we install are stored. +pub static TOOLCHAIN_DIRECTORY: Lazy = Lazy::new(|| { + std::env::var_os("UV_BOOTSTRAP_DIR").map_or( + Path::new(&std::env::var("CARGO_MANIFEST_DIR").unwrap()) + .parent() + .expect("CARGO_MANIFEST_DIR should be nested in workspace") + .parent() + .expect("CARGO_MANIFEST_DIR should be doubly nested in workspace") + .join("bin"), + PathBuf::from, + ) +}); + +/// An installed Python toolchain. +#[derive(Debug, Clone)] +pub struct Toolchain { + /// The path to the top-level directory of the installed toolchain. + path: PathBuf, +} + +impl Toolchain { + pub fn executable(&self) -> PathBuf { + if cfg!(windows) { + self.path.join("install").join("python.exe") + } else if cfg!(unix) { + self.path.join("install").join("bin").join("python3") + } else { + unimplemented!("Only Windows and Unix systems are supported.") + } + } +} + +/// Return the toolchains that satisfy the given Python version on this platform. +/// +/// ## Errors +/// +/// - The platform metadata cannot be read +/// - A directory in the toolchain directory cannot be read +pub fn toolchains_for_version(version: &PythonVersion) -> Result, Error> { + let platform_key = platform_key_from_env()?; + + // TODO(zanieb): Consider returning an iterator instead of a `Vec` + // Note we need to collect paths regardless for sorting by version. + + let toolchain_dirs = match fs_err::read_dir(TOOLCHAIN_DIRECTORY.to_path_buf()) { + Ok(toolchain_dirs) => { + // Collect sorted directory paths; `read_dir` is not stable across platforms + let directories: BTreeSet<_> = toolchain_dirs + .filter_map(|read_dir| match read_dir { + Ok(entry) => match entry.file_type() { + Ok(file_type) => file_type.is_dir().then_some(Ok(entry.path())), + Err(err) => Some(Err(err)), + }, + Err(err) => Some(Err(err)), + }) + .collect::>() + .map_err(|err| Error::ReadError { + dir: TOOLCHAIN_DIRECTORY.to_path_buf(), + err, + })?; + directories + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => { + return Ok(Vec::new()); + } + Err(err) => { + return Err(Error::ReadError { + dir: TOOLCHAIN_DIRECTORY.to_path_buf(), + err, + }) + } + }; + + Ok(toolchain_dirs + .into_iter() + // Sort "newer" versions of Python first + .rev() + .filter_map(|path| { + if path + .file_name() + .map(OsStr::to_string_lossy) + .is_some_and(|filename| { + filename.starts_with(&format!("cpython-{version}")) + && filename.ends_with(&platform_key) + }) + { + Some(Toolchain { path }) + } else { + None + } + }) + .collect::>()) +} + +/// Generate a platform portion of a key from the environment. +fn platform_key_from_env() -> Result { + let os = Os::from_env()?; + let arch = Arch::from_env()?; + let libc = Libc::from_env()?; + Ok(format!("{os}-{arch}-{libc}").to_lowercase()) +} diff --git a/crates/uv-toolchain/src/lib.rs b/crates/uv-toolchain/src/lib.rs new file mode 100644 index 000000000..116571ff7 --- /dev/null +++ b/crates/uv-toolchain/src/lib.rs @@ -0,0 +1,9 @@ +pub use crate::downloads::{ + DownloadResult, Error, Platform, PythonDownload, PythonDownloadRequest, +}; +pub use crate::find::{toolchains_for_version, Toolchain, TOOLCHAIN_DIRECTORY}; +pub use crate::python_version::PythonVersion; + +mod downloads; +mod find; +mod python_version; diff --git a/crates/uv-interpreter/src/python_version.rs b/crates/uv-toolchain/src/python_version.rs similarity index 92% rename from crates/uv-interpreter/src/python_version.rs rename to crates/uv-toolchain/src/python_version.rs index dffb46fbf..ba3d36385 100644 --- a/crates/uv-interpreter/src/python_version.rs +++ b/crates/uv-toolchain/src/python_version.rs @@ -5,8 +5,6 @@ use std::str::FromStr; use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, StringVersion}; -use crate::Interpreter; - #[derive(Debug, Clone)] pub struct PythonVersion(StringVersion); @@ -142,18 +140,6 @@ impl PythonVersion { Self::from_str(format!("{}.{}", self.major(), self.minor()).as_str()) .expect("dropping a patch should always be valid") } - - /// Check if this Python version is satisfied by the given interpreter. - /// - /// If a patch version is present, we will require an exact match. - /// Otherwise, just the major and minor version numbers need to match. - pub fn is_satisfied_by(&self, interpreter: &Interpreter) -> bool { - if self.patch().is_some() { - self.version() == interpreter.python_version() - } else { - (self.major(), self.minor()) == interpreter.python_tuple() - } - } } #[cfg(test)] diff --git a/crates/uv-toolchain/src/python_versions.inc b/crates/uv-toolchain/src/python_versions.inc new file mode 100644 index 000000000..56c2aeac0 --- /dev/null +++ b/crates/uv-toolchain/src/python_versions.inc @@ -0,0 +1,5467 @@ +// DO NOT EDIT +// +// Generated with `crates/uv-toolchain/template-version-metadata.py` +// From template at `crates/uv-toolchain/src/python_versions.inc.mustache` + +pub(crate) const PYTHON_DOWNLOADS: &[PythonDownload] = &[ + PythonDownload { + key: "cpython-3.12.2-linux-aarch64-gnu", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("469a7fd0d0a09936c5db41b5ac83bb29d5bfeb721aa483ac92f3f7ac4d311097") + }, + PythonDownload { + key: "cpython-3.12.2-macos-aarch64-none", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("2afcc8b25c55793f6ceb0bef2e547e101f53c9e25a0fe0332320e5381a1f0fdb") + }, + PythonDownload { + key: "cpython-3.12.2-linux-powerpc64le-gnu", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("1d70476fb9013cc93e787417680b34629b510e6e2145cf48bb2f0fe887f7a4d8") + }, + PythonDownload { + key: "cpython-3.12.2-linux-s390x-gnu", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("f40b88607928b5ee34ff87c1d574c8493a1604d7a40474e1b03731184186f419") + }, + PythonDownload { + key: "cpython-3.12.2-windows-x86-none", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("ee985ae6a6a98f4d5bd19fd8c59f45235911d19b64e1dbd026261b8103f15db5") + }, + PythonDownload { + key: "cpython-3.12.2-linux-x86_64-gnu", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("15b61ed9d33b35ad014a13a68a55d8ea5ba7fb70945644747f4e53c659f2fed6") + }, + PythonDownload { + key: "cpython-3.12.2-linux-x86_64-musl", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("7ec1dc7ad8223ec5839a57d232fd3cf730987f7b0f88b2c4f15ee935bcabbaa9") + }, + PythonDownload { + key: "cpython-3.12.2-macos-x86_64-none", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("b4b4d19c36e86803aa0b4410395f5568bef28d82666efba926e44dbe06345a12") + }, + PythonDownload { + key: "cpython-3.12.2-windows-x86_64-none", + major: 3, + minor: 12, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.12.2%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("a1daf5e8ceb23d34ea29b16b5123b06694810fe7acc5c8384426435c63bf731e") + }, + PythonDownload { + key: "cpython-3.12.1-linux-aarch64-gnu", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("9009da24f436611d0bf086b8ea62aaed1c27104af5b770ddcfc92b60db06da8c") + }, + PythonDownload { + key: "cpython-3.12.1-macos-aarch64-none", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("61e51e3490537b800fcefad718157cf775de41044e95aa538b63ab599f66f3a9") + }, + PythonDownload { + key: "cpython-3.12.1-linux-powerpc64le-gnu", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("b61686ce05c58c913e4fdb7e7c7105ed36d9bcdcd1a841e7f08b243f40d5cf77") + }, + PythonDownload { + key: "cpython-3.12.1-linux-s390x-gnu", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("505a4fbace661a43b354a059022eb31efb406859a5f7227109ebf0f278f20503") + }, + PythonDownload { + key: "cpython-3.12.1-windows-x86-none", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("22866d35fdf58e90e75d6ba9aa78c288b452ea7041fa9bc5549eca9daa431883") + }, + PythonDownload { + key: "cpython-3.12.1-linux-x86_64-gnu", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("89ef67b617b8c9804965509b2d256f53439ceede83b5b64085315f038ad81e60") + }, + PythonDownload { + key: "cpython-3.12.1-linux-x86_64-musl", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("c4b07a02d8f0986b56e010a67132e5eeba1def4991c6c06ed184f831a484a06f") + }, + PythonDownload { + key: "cpython-3.12.1-macos-x86_64-none", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("bf2b176b0426d7b4d4909c1b19bbb25b4893f9ebdc61e32df144df2b10dcc800") + }, + PythonDownload { + key: "cpython-3.12.1-windows-x86_64-none", + major: 3, + minor: 12, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.12.1%2B20240107-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("d9bc1b566250bf51818976bf98bf50e1f4c59b2503b50d29250cac5ab5ef6b38") + }, + PythonDownload { + key: "cpython-3.12.0-linux-aarch64-gnu", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("eb05c976374a9a44596ce340ab35e5461014f30202c3cbe10edcbfbe5ac4a6a1") + }, + PythonDownload { + key: "cpython-3.12.0-macos-aarch64-none", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("25fc8cd41e975d18d13bcc8f8beffa096ff8a0b86c4a737e1c6617900092c966") + }, + PythonDownload { + key: "cpython-3.12.0-linux-powerpc64le-gnu", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("800a89873e30e24bb1b6075f8cd718964537c5ba62bcdbefdcdae4de68ddccc4") + }, + PythonDownload { + key: "cpython-3.12.0-linux-s390x-gnu", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("5b1a1effbb43df57ad014fcebf4b20089e504d89613e7b8db22d9ccb9fb00a6c") + }, + PythonDownload { + key: "cpython-3.12.0-windows-x86-none", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("465e91b6e6d0d1c40c8a4bce3642c4adcb9b75cf03fbd5fd5a33a36358249289") + }, + PythonDownload { + key: "cpython-3.12.0-linux-x86_64-gnu", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("a8c38cd2e53136c579632e2938d1b857f22e496c7dba99ad9a7ad6a67b43274a") + }, + PythonDownload { + key: "cpython-3.12.0-linux-x86_64-musl", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("91b42595cb4b69ff396e746dc492caf67b952a3ed1a367a4ace1acc965ed9cdb") + }, + PythonDownload { + key: "cpython-3.12.0-macos-x86_64-none", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("3b4781e7fd4efabe574ba0954e54c35c7d5ac4dc5b2990b40796c1c6aec67d79") + }, + PythonDownload { + key: "cpython-3.12.0-windows-x86_64-none", + major: 3, + minor: 12, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.12.0%2B20231002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("5bdff7ed56550d96f9b26a27a8c25f0cc58a03bff19e5f52bba84366183cab8b") + }, + PythonDownload { + key: "cpython-3.11.8-linux-aarch64-gnu", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("45bf082aca6b7d5e7261852720a72b92f5305e9fdb07b10f6588cb51d8f83ff2") + }, + PythonDownload { + key: "cpython-3.11.8-macos-aarch64-none", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("c0650884b929253b8688797d1955850f6e339bf0428b3d935f62ab3159f66362") + }, + PythonDownload { + key: "cpython-3.11.8-linux-powerpc64le-gnu", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("a9716f2eebebe03de47d6d5d603d6ff78abf5eb38f88bf7607b17fd85e74ff16") + }, + PythonDownload { + key: "cpython-3.11.8-linux-s390x-gnu", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("d495830b5980ed689bd7588aa556bac9c43ff766d8a8b32e7791b8ed664b04f3") + }, + PythonDownload { + key: "cpython-3.11.8-windows-x86-none", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("c3e90962996177a027bd73dd9fd8c42a2d6ef832cda26db4ab4efc6105160537") + }, + PythonDownload { + key: "cpython-3.11.8-linux-x86_64-gnu", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("d959c43184878d564b5368ce4d753cf059600aafdf3e50280e850f94b5a4ba61") + }, + PythonDownload { + key: "cpython-3.11.8-linux-x86_64-musl", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("a03a9d8c1f770ce418716a2e8185df7b3a9e0012cdc220f9f2d24480a432650b") + }, + PythonDownload { + key: "cpython-3.11.8-macos-x86_64-none", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("54f8c8ad7313b3505e495bb093825d85eab244306ca4278836a2c7b5b74fb053") + }, + PythonDownload { + key: "cpython-3.11.8-windows-x86_64-none", + major: 3, + minor: 11, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.11.8%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("6da82390f7ac49f6c4b19a5b8019c4ddc1eef2c5ad6a2f2d32773a27663a4e14") + }, + PythonDownload { + key: "cpython-3.11.7-linux-aarch64-gnu", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("e3a375f8f16198ccf8dbede231536544265e5b4b6b0f0df97c5b29503c5864e2") + }, + PythonDownload { + key: "cpython-3.11.7-macos-aarch64-none", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("c1f3dd13825906a5eae23ed8de9b653edb620568b2e0226eef3784eb1cce7eed") + }, + PythonDownload { + key: "cpython-3.11.7-linux-powerpc64le-gnu", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("016ed6470c599ea5cc4dbb9c3f3fe86be059ad4e1b6cd2df10e40b7ec6970f16") + }, + PythonDownload { + key: "cpython-3.11.7-linux-s390x-gnu", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("91b33369025b7e0079f603cd2a99f9a5932daa8ded113d5090f29c075c993df7") + }, + PythonDownload { + key: "cpython-3.11.7-windows-x86-none", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("6613f1f9238d19969d8a2827deec84611cb772503207056cc9f0deb89bea48cd") + }, + PythonDownload { + key: "cpython-3.11.7-linux-x86_64-gnu", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("01bca7a2f457d4bd2b367640d9337d12b31db73d670a16500b7a751194942103") + }, + PythonDownload { + key: "cpython-3.11.7-linux-x86_64-musl", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("f387d373d64447bbba8a5657712f93b1dbdfd7246cdfe5a0493f39b83d46ec7c") + }, + PythonDownload { + key: "cpython-3.11.7-macos-x86_64-none", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("3f8caf73f2bfe22efa9666974c119727e163716e88af8ed3caa1e0ae5493de61") + }, + PythonDownload { + key: "cpython-3.11.7-windows-x86_64-none", + major: 3, + minor: 11, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240107/cpython-3.11.7%2B20240107-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("89d1d8f080e5494ea57918fc5ecf3d483ffef943cd5a336e64da150cd44b4aa0") + }, + PythonDownload { + key: "cpython-3.11.6-linux-aarch64-gnu", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("d63d6eb065e60899b25853fe6bbd9f60ea6c3b12f4854adc75cb818bad55f4e9") + }, + PythonDownload { + key: "cpython-3.11.6-macos-aarch64-none", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("6e9007bcbbf51203e89c34a87ed42561630a35bc4eb04a565c92ba7159fe5826") + }, + PythonDownload { + key: "cpython-3.11.6-linux-powerpc64le-gnu", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("71c34db1165860a6bf458d817aef00dea96146130bf5f8bd7ee39b12892ef463") + }, + PythonDownload { + key: "cpython-3.11.6-linux-s390x-gnu", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("78252aa883fed18de7bb9b146450e42dd75d78c345f56c1301bb042317a1d4f7") + }, + PythonDownload { + key: "cpython-3.11.6-windows-x86-none", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("2670731428191d4476bf260c8144ccf06f9e5f8ac6f2de1dc444ca96ab627082") + }, + PythonDownload { + key: "cpython-3.11.6-linux-x86_64-gnu", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("6e7889a15d861f1860ed84f3f5ea4586d198aa003b22556d91e180a44184dcd7") + }, + PythonDownload { + key: "cpython-3.11.6-linux-x86_64-musl", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("1b6e32ec93c5a18a03a9da9e2a3a3738d67b733df0795edcff9fd749c33ab931") + }, + PythonDownload { + key: "cpython-3.11.6-macos-x86_64-none", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("3685156e4139e89484c071ba1a1b85be0b4e302a786de5a170d3b0713863c2e8") + }, + PythonDownload { + key: "cpython-3.11.6-windows-x86_64-none", + major: 3, + minor: 11, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20231002/cpython-3.11.6%2B20231002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("38d2c2fa2f9effbf486207bef7141d1b5c385ad30729ab0c976e6a852a2a9401") + }, + PythonDownload { + key: "cpython-3.11.5-linux-aarch64-gnu", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("ac4b1e91d1cb7027595bfa4667090406331b291b2e346fb74e42b7031b216787") + }, + PythonDownload { + key: "cpython-3.11.5-macos-aarch64-none", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("7bee180b764722a73c2599fbe2c3a6121cf6bbcb08cb3082851e93c43fe130e7") + }, + PythonDownload { + key: "cpython-3.11.5-linux-powerpc64le-gnu", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("1aee6a613385a6355bed61a9b12259a5ed16e871b5bdfe5c9fe98b46ee2bb05e") + }, + PythonDownload { + key: "cpython-3.11.5-linux-s390x-gnu", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("b0819032ec336d6e1d9e9bfdba546bf854a7b7248f8720a6d07da72c4ac927e5") + }, + PythonDownload { + key: "cpython-3.11.5-linux-x86-gnu", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("75d27b399b323c25d8250fda9857e388bf1b03ba1eb7925ec23cf12042a63a88") + }, + PythonDownload { + key: "cpython-3.11.5-windows-x86-none", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("c9ffe9c2c88685ce3064f734cbdfede0a07de7d826fada58f8045f3bd8f81a9d") + }, + PythonDownload { + key: "cpython-3.11.5-linux-x86_64-gnu", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("93ee095b53de5a74af18e612f55095fcf3118c3c0a87eb6344d8eaca396bfb2d") + }, + PythonDownload { + key: "cpython-3.11.5-linux-x86_64-musl", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("9dcf19ee54fb936cb9fd0f02fd655e790663534bc12e142e460c1b30a0b54dbd") + }, + PythonDownload { + key: "cpython-3.11.5-macos-x86_64-none", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("e43d70a49919641ca2939a5a9107b13d5fef8c13af0f511a33a94bb6af2044f0") + }, + PythonDownload { + key: "cpython-3.11.5-windows-x86_64-none", + major: 3, + minor: 11, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.11.5%2B20230826-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("6e4d20e6d498f9edeb3c28cb9541ad20f675f16da350b078e40a9dcfd93cdc3d") + }, + PythonDownload { + key: "cpython-3.11.4-linux-aarch64-gnu", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("37cf00439b57adf7ffef4a349d62dcf09739ba67b670e903b00b25f81fbb8a68") + }, + PythonDownload { + key: "cpython-3.11.4-macos-aarch64-none", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("988d476c806f71a3233ff4266eda166a5d28cf83ba306ac88b4220554fc83e8c") + }, + PythonDownload { + key: "cpython-3.11.4-linux-powerpc64le-gnu", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("b9f76fd226bfcbc6a8769934b17323ca3b563f1c24660582fcccfa6d0c7146af") + }, + PythonDownload { + key: "cpython-3.11.4-linux-s390x-gnu", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("8ef6b5fa86b4abf51865b346b7cf8df36e474ed308869fc0ac3fe82de39194a4") + }, + PythonDownload { + key: "cpython-3.11.4-linux-x86-gnu", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("a9051364b5c2e28205f8484cae03d16c86b45df5d117324e846d0f5e870fe9fb") + }, + PythonDownload { + key: "cpython-3.11.4-windows-x86-none", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("0d22f43c5bb3f27ff2f9e8c60b0d7abd391bb2cac1790b0960970ff5580f6e9a") + }, + PythonDownload { + key: "cpython-3.11.4-linux-x86_64-gnu", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("1b5fdeb2dc56c30843e7350f1684178755fae91666a0a987e5eb39074c42a052") + }, + PythonDownload { + key: "cpython-3.11.4-linux-x86_64-musl", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("fc2ea02ced875c90b8d025b409d58c4f045df8ba951bfa2b8b0a3cfe11c3b41c") + }, + PythonDownload { + key: "cpython-3.11.4-macos-x86_64-none", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("6d9765785316c7f1c07def71b413c92c84302f798b30ee09e2e0b5da28353a51") + }, + PythonDownload { + key: "cpython-3.11.4-windows-x86_64-none", + major: 3, + minor: 11, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.11.4%2B20230726-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("1692d795d6199b2261161ae54250009ffad0317929302903f6f2c773befd4d76") + }, + PythonDownload { + key: "cpython-3.11.3-linux-aarch64-gnu", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("991521082b0347878ba855c4986d77cc805c22ef75159bc95dd24bfd80275e27") + }, + PythonDownload { + key: "cpython-3.11.3-macos-aarch64-none", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("cd296d628ceebf55a78c7f6a7aed379eba9dbd72045d002e1c2c85af0d6f5049") + }, + PythonDownload { + key: "cpython-3.11.3-linux-powerpc64le-gnu", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("241d583be3ecc34d76fafa0d186cb504ce5625eb2c0e895dc4f4073a649e5c73") + }, + PythonDownload { + key: "cpython-3.11.3-linux-x86-gnu", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("7bd694eb848328e96f524ded0f9b9eca6230d71fce3cd49b335a5c33450f3e04") + }, + PythonDownload { + key: "cpython-3.11.3-windows-x86-none", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("877c90ef778a526aa25ab417034f5e70728ac14e5eb1fa5cfd741f531203a3fc") + }, + PythonDownload { + key: "cpython-3.11.3-linux-x86_64-gnu", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("4f1192179e1f62e69b8b45f7f699e6f0100fb0b8a39aad7a48472794d0c24bd4") + }, + PythonDownload { + key: "cpython-3.11.3-linux-x86_64-musl", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("8c5adef5bc627f39e93b920af86ef740e917aa698530ff727978d446a07bbd8b") + }, + PythonDownload { + key: "cpython-3.11.3-macos-x86_64-none", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("2fbb31a8bc6663e2d31d3054319b51a29b1915c03222a94b9d563233e11d1bef") + }, + PythonDownload { + key: "cpython-3.11.3-windows-x86_64-none", + major: 3, + minor: 11, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.11.3%2B20230507-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("9d27e607fb1cb2d766e17f27853013d8c0f0b09ac53127aaff03ec89ab13370d") + }, + PythonDownload { + key: "cpython-3.11.1-linux-aarch64-gnu", + major: 3, + minor: 11, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("8fe27d850c02aa7bb34088fad5b48df90b4b841f40e1472243b8ab9da8776e40") + }, + PythonDownload { + key: "cpython-3.11.1-macos-aarch64-none", + major: 3, + minor: 11, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("da187194cc351d827232b1d2d85b2855d7e25a4ada3e47bc34b4f87b1d989be5") + }, + PythonDownload { + key: "cpython-3.11.1-linux-x86-gnu", + major: 3, + minor: 11, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("7986ebe82c07ecd2eb94fd1b3c9ebbb2366db2360e38f29ae0543e857551d0bf") + }, + PythonDownload { + key: "cpython-3.11.1-windows-x86-none", + major: 3, + minor: 11, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("b062ac2c72a85510fb9300675bd5c716baede21e9482ef6335247b4aa006584c") + }, + PythonDownload { + key: "cpython-3.11.1-linux-x86_64-gnu", + major: 3, + minor: 11, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("b5bf700afc77588d853832d10b74ba793811cbec41b02ebc2c39a8b9987aacdd") + }, + PythonDownload { + key: "cpython-3.11.1-linux-x86_64-musl", + major: 3, + minor: 11, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("ec5da5b428f6d91d96cde2621c0380f67bb96e4257d2628bc70b50e75ec5f629") + }, + PythonDownload { + key: "cpython-3.11.1-macos-x86_64-none", + major: 3, + minor: 11, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("0eb61be53ee13cf75a30b8a164ef513a2c7995b25b118a3a503245d46231b13a") + }, + PythonDownload { + key: "cpython-3.11.1-windows-x86_64-none", + major: 3, + minor: 11, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.11.1%2B20230116-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("f5c46fffda7d7894b975af728f739b02d1cec50fd4a3ea49f69de9ceaae74b17") + }, + PythonDownload { + key: "cpython-3.10.13-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("06a53040504e1e2fdcb32dc0d61b123bea76725b5c14031c8f64e28f52ae5a5f") + }, + PythonDownload { + key: "cpython-3.10.13-macos-aarch64-none", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("57b83a4aa32bdbe7611f1290313ef24f2574dff5fa59181c0ccb26c14c688b73") + }, + PythonDownload { + key: "cpython-3.10.13-linux-powerpc64le-gnu", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("cab4c8756445d1d1987c7c94d3bcf323684e44fb9070329d8287d4c38e155711") + }, + PythonDownload { + key: "cpython-3.10.13-linux-s390x-gnu", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("fe46914541126297c7a8636845c2e7188868eaa617bb6e293871fca4a5cb63f7") + }, + PythonDownload { + key: "cpython-3.10.13-linux-x86-gnu", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.10.13%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("08a3a1ff61b7ed2c87db7a9f88630781d98fabc2efb499f38ae0ead05973eb56") + }, + PythonDownload { + key: "cpython-3.10.13-windows-x86-none", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("c8b99dcf267c574fdfbdf4e9d63ec7a4aa4608565fee3fba0b2f73843b9713b2") + }, + PythonDownload { + key: "cpython-3.10.13-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("41b20e9d87f57d27f608685b714a57eea81c9e079aa647d59837ec6659536626") + }, + PythonDownload { + key: "cpython-3.10.13-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("fd18e6039be25bf23d13caf5140569df71d61312b823b715b3c788747fec48e9") + }, + PythonDownload { + key: "cpython-3.10.13-macos-x86_64-none", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("a41c1e28e2a646bac69e023873d40a43c5958d251c6adfa83d5811a7cb034c7a") + }, + PythonDownload { + key: "cpython-3.10.13-windows-x86_64-none", + major: 3, + minor: 10, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.10.13%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("6a2c8f37509556e5d463b1f437cdf7772ebd84cdf183c258d783e64bb3109505") + }, + PythonDownload { + key: "cpython-3.10.12-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("e30f2b4fd9bd79b9122e2975f3c17c9ddd727f8326b2e246378e81f7ecc7d74f") + }, + PythonDownload { + key: "cpython-3.10.12-macos-aarch64-none", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("a7d0cadbe867cc53dd47d7327244154157a7cca02edb88cf3bb760a4f91d4e44") + }, + PythonDownload { + key: "cpython-3.10.12-linux-powerpc64le-gnu", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("c318050fa91d84d447f5c8a5887a44f1cc8dd34d4c1d357cd755407d46ed1b21") + }, + PythonDownload { + key: "cpython-3.10.12-linux-s390x-gnu", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("756579b52acb9b13b162ac901e56ff311def443e69d7f7259a91198b76a30ecb") + }, + PythonDownload { + key: "cpython-3.10.12-linux-x86-gnu", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("89c83fcdfd41c67e2dd2a037982556c657dc55fc1938c6f6cdcd5ffa614c1fb3") + }, + PythonDownload { + key: "cpython-3.10.12-windows-x86-none", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("0743b9976f20b06d9cf12de9d1b2dfe06b13f76978275e9dac73a275624bde2c") + }, + PythonDownload { + key: "cpython-3.10.12-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("fb7354fcee7b17dd0793ebd3f6f1fc8b7b205332afcf8d700cc1119f2dc33ff7") + }, + PythonDownload { + key: "cpython-3.10.12-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("b343cbe7c41b7698b568ea5252328cdccb213100efa71da8d3db6e21afd9f6cf") + }, + PythonDownload { + key: "cpython-3.10.12-macos-x86_64-none", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("f1fa448384dd48033825e56ee6b5afc76c5dd67dcf2b73b61d2b252ae2e87bca") + }, + PythonDownload { + key: "cpython-3.10.12-windows-x86_64-none", + major: 3, + minor: 10, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.10.12%2B20230726-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("cb6e7c84d9e369a0ee76c9ea73d415a113ba9982db58f44e6bab5414838d35f3") + }, + PythonDownload { + key: "cpython-3.10.11-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("a5271cc014f2ce2ab54a0789556c15b84668e2afcc530512818c4b87c6a94483") + }, + PythonDownload { + key: "cpython-3.10.11-macos-aarch64-none", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("da9c8a3cd04485fd397387ea2fa56f3cac71827aafb51d8438b2868f86eb345b") + }, + PythonDownload { + key: "cpython-3.10.11-linux-powerpc64le-gnu", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("ac32e3788109ff0cc536a6108072d9203217df744cf56d3a4ab0b19857d8e244") + }, + PythonDownload { + key: "cpython-3.10.11-linux-x86-gnu", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("9304d6eeef48bd246a2959ebc76b20dbb2c6a81aa1d214f4471cb273c11717f2") + }, + PythonDownload { + key: "cpython-3.10.11-windows-x86-none", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("60e76e136ab23b891ed1212e58bd11a73a19cd9fd884ec1c5653ca1c159d674e") + }, + PythonDownload { + key: "cpython-3.10.11-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("544e5020f71ad1525dbc92b08e429cc1e1e11866c48c07d91e99f531b9ba68b0") + }, + PythonDownload { + key: "cpython-3.10.11-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("7918188e01a266915dd0945711e274d45c8d7fb540d48240e13c4fd96f43afbb") + }, + PythonDownload { + key: "cpython-3.10.11-macos-x86_64-none", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("e84c12aa0285235eed365971ceedf040f4d8014f5342d371e138a4da9e4e9b7c") + }, + PythonDownload { + key: "cpython-3.10.11-windows-x86_64-none", + major: 3, + minor: 10, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.10.11%2B20230507-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("9b4dc4a335b6122ce783bc80f5015b683e3ab1a56054751c5df494db0521da67") + }, + PythonDownload { + key: "cpython-3.10.9-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("2c0996dd1fe35314e06e042081b24fb53f3b7b361c3e1b94a6ed659c275ca069") + }, + PythonDownload { + key: "cpython-3.10.9-macos-aarch64-none", + major: 3, + minor: 10, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("2508b8d4b725bb45c3e03d2ddd2b8441f1a74677cb6bd6076e692c0923135ded") + }, + PythonDownload { + key: "cpython-3.10.9-linux-x86-gnu", + major: 3, + minor: 10, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("f8c3a63620f412c4a9ccfb6e2435a96a55775550c81a452d164caa6d03a6a1da") + }, + PythonDownload { + key: "cpython-3.10.9-windows-x86-none", + major: 3, + minor: 10, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("3d79cfd229ec12b678bbfd79c30fb4cbad9950d6bfb29741d2315b11839998b4") + }, + PythonDownload { + key: "cpython-3.10.9-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("a90a45ba7afcbd1df9aef96a614acbb210607299ac74dadbb6bd66af22be34db") + }, + PythonDownload { + key: "cpython-3.10.9-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("1310f187a73b00164ec4ca34e643841c5c34cbb93fe0b3a3f9504e5ea5001ec7") + }, + PythonDownload { + key: "cpython-3.10.9-macos-x86_64-none", + major: 3, + minor: 10, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("1153b4d3b03cf1e1d8ec93c098160586f665fcc2d162c0812140a716a688df58") + }, + PythonDownload { + key: "cpython-3.10.9-windows-x86_64-none", + major: 3, + minor: 10, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230116/cpython-3.10.9%2B20230116-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("4cfa6299a78a3959102c461d126e4869616f0a49c60b44220c000fc9aecddd78") + }, + PythonDownload { + key: "cpython-3.10.8-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("879e76260be226512693e37a28cc3a6670b5ee270a4440e4b04a7b415dba451c") + }, + PythonDownload { + key: "cpython-3.10.8-macos-aarch64-none", + major: 3, + minor: 10, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("f8ba5f87153a17717e900ff7bba20e2eefe8a53a5bd3c78f9f6922d6d910912d") + }, + PythonDownload { + key: "cpython-3.10.8-linux-x86-gnu", + major: 3, + minor: 10, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("ab434eccffeec4f6f51af017e4eed69d4f1ea55f48c5b89b8a8779df3fa799df") + }, + PythonDownload { + key: "cpython-3.10.8-windows-x86-none", + major: 3, + minor: 10, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("7547ea172f7fa3d7619855f28780da9feb615b6cb52c5c64d34f65b542799fee") + }, + PythonDownload { + key: "cpython-3.10.8-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("c86182951a82e761588476a0155afe99ae4ae1030e4a8e1e8bcb8e1d42f6327c") + }, + PythonDownload { + key: "cpython-3.10.8-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("bb87e933afcfd2e8de045e5a691feff1fb8fb06a09315b37d187762fddfc4546") + }, + PythonDownload { + key: "cpython-3.10.8-macos-x86_64-none", + major: 3, + minor: 10, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("a18f81ecc7da0779be960ad35c561a834866c0e6d1310a4f742fddfd6163753f") + }, + PythonDownload { + key: "cpython-3.10.8-windows-x86_64-none", + major: 3, + minor: 10, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.10.8%2B20221106-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("ab40f9584be896c697c5fca351ab82d7b55f01b8eb0494f0a15a67562e49161a") + }, + PythonDownload { + key: "cpython-3.10.7-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("9f346729b523e860194635eb67c9f6bc8f12728ba7ddfe4fd80f2e6d685781e3") + }, + PythonDownload { + key: "cpython-3.10.7-macos-aarch64-none", + major: 3, + minor: 10, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("9f44cf63441a90f4ec99a032a2bda43971ae7964822daa0ee730a9cba15d50da") + }, + PythonDownload { + key: "cpython-3.10.7-linux-x86-gnu", + major: 3, + minor: 10, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("a79816c50abeb2752530f68b4d7d95b6f48392f44a9a7f135b91807d76872972") + }, + PythonDownload { + key: "cpython-3.10.7-windows-x86-none", + major: 3, + minor: 10, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("323532701cb468199d6f14031b991f945d4bbf986ca818185e17e132d3763bdf") + }, + PythonDownload { + key: "cpython-3.10.7-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("ce3fe27e6ca3a0e75a7f4f3b6568cd1bf967230a67e73393e94a23380dddaf10") + }, + PythonDownload { + key: "cpython-3.10.7-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("7f2c933d23c0f38cf145c2d6c65b5cf53bb589690d394fd4c01b2230c23c2bff") + }, + PythonDownload { + key: "cpython-3.10.7-macos-x86_64-none", + major: 3, + minor: 10, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("e03e28dc9fe55ea5ca06fece8f2f2a16646b217d28c0cd09ebcd512f444fdc90") + }, + PythonDownload { + key: "cpython-3.10.7-windows-x86_64-none", + major: 3, + minor: 10, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.10.7%2B20221002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("5363974e6ee6c91dbd6bc3533e38b02a26abc2ff1c9a095912f237b916be22d3") + }, + PythonDownload { + key: "cpython-3.10.6-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("edc1c9742b824caebbc5cb224c8990aa8658b81593fd9219accf3efa3e849501") + }, + PythonDownload { + key: "cpython-3.10.6-macos-aarch64-none", + major: 3, + minor: 10, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("159230851a69cf5cab80318bce48674244d7c6304de81f44c22ff0abdf895cfa") + }, + PythonDownload { + key: "cpython-3.10.6-linux-x86-gnu", + major: 3, + minor: 10, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("07fa4f5499b8885d1eea49caf5476d76305ab73494b7398dfd22c14093859e4f") + }, + PythonDownload { + key: "cpython-3.10.6-windows-x86-none", + major: 3, + minor: 10, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("8d9a259e15d5a1be48ef13cd5627d7f6c15eadf41a3539e99ed1deee668c075e") + }, + PythonDownload { + key: "cpython-3.10.6-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("407e5951e39f5652b32b72b715c4aa772dd8c2da1065161c58c30a1f976dd1b2") + }, + PythonDownload { + key: "cpython-3.10.6-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("f859a72da0bb2f1261f8cebdac931b05b59474c7cb65cee8e85c34fc014dd452") + }, + PythonDownload { + key: "cpython-3.10.6-macos-x86_64-none", + major: 3, + minor: 10, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("9405499573a7aa8b67d070d096ded4f3e571f18c2b34762606ecc8025290b122") + }, + PythonDownload { + key: "cpython-3.10.6-windows-x86_64-none", + major: 3, + minor: 10, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.10.6%2B20220802-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("01dc349721594b1bb5b582651f81479a24352f718fdf6279101caa0f377b160a") + }, + PythonDownload { + key: "cpython-3.10.5-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("9fa6970a3d0a5dc26c4ed272bb1836d1f1f7a8f4b9d67f634d0262ff8c1fed0b") + }, + PythonDownload { + key: "cpython-3.10.5-macos-aarch64-none", + major: 3, + minor: 10, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("f68d25dbe9daa96187fa9e05dd8969f46685547fecf1861a99af898f96a5379e") + }, + PythonDownload { + key: "cpython-3.10.5-linux-x86-gnu", + major: 3, + minor: 10, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("63fcfc425adabc034c851dadfb499de3083fd7758582191c12162ad2471256b0") + }, + PythonDownload { + key: "cpython-3.10.5-windows-x86-none", + major: 3, + minor: 10, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("e201192f0aa73904bc5a5f43d1ce4c9fb243dfe02138e690676713fe02c7d662") + }, + PythonDownload { + key: "cpython-3.10.5-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("f8dfb83885d1cbc82febfa613258c1f6954ea88ef43ed7dc710d6df20efecdab") + }, + PythonDownload { + key: "cpython-3.10.5-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("3682e0add14a3bac654afe467a84981628b0c7ebdccd4ebf26dfaa916238e2fe") + }, + PythonDownload { + key: "cpython-3.10.5-macos-x86_64-none", + major: 3, + minor: 10, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("5e372e6738a733532aa985730d9a47ee4c77b7c706e91ef61d37aacbb2e54845") + }, + PythonDownload { + key: "cpython-3.10.5-windows-x86_64-none", + major: 3, + minor: 10, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220630/cpython-3.10.5%2B20220630-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("cff35feefe423d4282e9a3e1bb756d0acbb2f776b1ada82c44c71ac3e1491448") + }, + PythonDownload { + key: "cpython-3.10.4-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("092369e9d170c4c1074e1b305accb74f9486e6185d2e3f3f971869ff89538d3e") + }, + PythonDownload { + key: "cpython-3.10.4-macos-aarch64-none", + major: 3, + minor: 10, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("c404f226195d79933b1e0a3ec88f0b79d35c873de592e223e11008f3a37f83d6") + }, + PythonDownload { + key: "cpython-3.10.4-linux-x86-gnu", + major: 3, + minor: 10, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("ba940a74a7434fe78d81aed9fb1e5ccdc3d97191a2db35716fc94e3b6604ace0") + }, + PythonDownload { + key: "cpython-3.10.4-windows-x86-none", + major: 3, + minor: 10, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("c37a47e46de93473916f700a790cb43515f00745fba6790004e2731ec934f4d3") + }, + PythonDownload { + key: "cpython-3.10.4-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("7699f76ef89b436b452eacdbab508da3cd94146ba29b099f5cb6e250afba3210") + }, + PythonDownload { + key: "cpython-3.10.4-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("8b8b97f7746a3deca91ada408025457ced34f582dad2114b33ce6fec9cf35b28") + }, + PythonDownload { + key: "cpython-3.10.4-macos-x86_64-none", + major: 3, + minor: 10, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("e447f00fe53168d18cbfe110645dbf33982a17580b9e4424a411f9245d99cd21") + }, + PythonDownload { + key: "cpython-3.10.4-windows-x86_64-none", + major: 3, + minor: 10, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220528/cpython-3.10.4%2B20220528-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("d636dc1bcca74dd9c6e3b26f7c081b3e229336e8378fe554bf8ba65fe780a2ac") + }, + PythonDownload { + key: "cpython-3.10.3-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("101284d27578438da200be1f6b9a1ba621432c5549fa5517797ec320bf75e3d5") + }, + PythonDownload { + key: "cpython-3.10.3-macos-aarch64-none", + major: 3, + minor: 10, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("b1abefd0fc66922cf9749e4d5ceb97df4d3cfad0cd9cdc4bd04262a68d565698") + }, + PythonDownload { + key: "cpython-3.10.3-linux-x86-gnu", + major: 3, + minor: 10, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("43c1cd6e203bfba1a2eeb96cd2a15ce0ebde0e72ecc9555934116459347a9c28") + }, + PythonDownload { + key: "cpython-3.10.3-windows-x86-none", + major: 3, + minor: 10, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("fbc0924a138937fe435fcdb20b0c6241290558e07f158e5578bd91cc8acef469") + }, + PythonDownload { + key: "cpython-3.10.3-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("04760d869234ee8f801feb08edc042a6965320f6c0a7aedf92ec35501fef3b21") + }, + PythonDownload { + key: "cpython-3.10.3-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("7c034d8a5787744939335ce43d64f2ddcc830a74e63773408d0c8f3c3a4e7916") + }, + PythonDownload { + key: "cpython-3.10.3-macos-x86_64-none", + major: 3, + minor: 10, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("bc5d6f284b506104ff6b4e36cec84cbdb4602dfed4c6fe19971a808eb8c439ec") + }, + PythonDownload { + key: "cpython-3.10.3-windows-x86_64-none", + major: 3, + minor: 10, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.10.3%2B20220318-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("72b91d26f54321ba90a86a3bbc711fa1ac31e0704fec352b36e70b0251ffb13c") + }, + PythonDownload { + key: "cpython-3.10.2-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("9936f1549f950311229465de509b35c062aa474e504c20a1d6f0f632da57e002") + }, + PythonDownload { + key: "cpython-3.10.2-macos-aarch64-none", + major: 3, + minor: 10, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("1ef939fd471a9d346a7bc43d2c16fb483ddc4f98af6dad7f08a009e299977a1a") + }, + PythonDownload { + key: "cpython-3.10.2-linux-x86-gnu", + major: 3, + minor: 10, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("9be2a667f29ed048165cfb3f5dbe61703fd3e5956f8f517ae098740ac8411c0b") + }, + PythonDownload { + key: "cpython-3.10.2-windows-x86-none", + major: 3, + minor: 10, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("698b09b1b8321a4dc43d62f6230b62adcd0df018b2bcf5f1b4a7ce53dcf23bcc") + }, + PythonDownload { + key: "cpython-3.10.2-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("d22d85f60b2ef982b747adda2d1bde4a32c23c3d8f652c00ce44526750859e4e") + }, + PythonDownload { + key: "cpython-3.10.2-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("df246cf27db346081935d33ce0344a185d1f08b04a4500eb1e21d4d922ee7eb4") + }, + PythonDownload { + key: "cpython-3.10.2-macos-x86_64-none", + major: 3, + minor: 10, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("bacf720c13ab67685a384f1417e9c2420972d88f29c8b7c26e72874177f2d120") + }, + PythonDownload { + key: "cpython-3.10.2-windows-x86_64-none", + major: 3, + minor: 10, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.10.2%2B20220227-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("7397e78a4fbe429144adc1f33af942bdd5175184e082ac88f3023b3a740dd1a0") + }, + PythonDownload { + key: "cpython-3.10.0-linux-aarch64-gnu", + major: 3, + minor: 10, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-unknown-linux-gnu-debug-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.10.0-macos-aarch64-none", + major: 3, + minor: 10, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.10.0-linux-x86-gnu", + major: 3, + minor: 10, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-i686-unknown-linux-gnu-debug-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.10.0-windows-x86-none", + major: 3, + minor: 10, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-i686-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.10.0-linux-x86_64-gnu", + major: 3, + minor: 10, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-unknown-linux-gnu-debug-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.10.0-linux-x86_64-musl", + major: 3, + minor: 10, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-unknown-linux-musl-lto-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.10.0-macos-x86_64-none", + major: 3, + minor: 10, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.10.0-windows-x86_64-none", + major: 3, + minor: 10, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.10.0-x86_64-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.18-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("d27efd4609a3e15ff901040529d5689be99f2ebfe5132ab980d066d775068265") + }, + PythonDownload { + key: "cpython-3.9.18-macos-aarch64-none", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("579f9b68bbb3a915cbab9682e4d3c253bc96b0556b8a860982c49c25c61f974a") + }, + PythonDownload { + key: "cpython-3.9.18-linux-powerpc64le-gnu", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("c138eef19229351226a11e752230b8aa9d499ba9720f9f0574fa3260ccacb99b") + }, + PythonDownload { + key: "cpython-3.9.18-linux-s390x-gnu", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("dd05eff699ce5a7eee545bc05e4869c4e64ee02bf0c70691bcee215604c6b393") + }, + PythonDownload { + key: "cpython-3.9.18-linux-x86-gnu", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.9.18%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("7faf8fdfbad04e0356a9d52c9b8be4d40ffef85c9ab3e312c45bd64997ef8aa9") + }, + PythonDownload { + key: "cpython-3.9.18-windows-x86-none", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("212d413ab6f854f588cf368fdd2aa140bb7c7ee930e3f7ac1002cba1e50e9685") + }, + PythonDownload { + key: "cpython-3.9.18-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("e1fa92798fab6f3b44a48f24b8e284660c34738d560681b206f0deb0616465f9") + }, + PythonDownload { + key: "cpython-3.9.18-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("8bf88ae2100e609902d98ec775468e3a41a834f6528e632d6d971f5f75340336") + }, + PythonDownload { + key: "cpython-3.9.18-macos-x86_64-none", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("146537b9b4a1baa672eed94373e149ca1ee339c4df121e8916d8436265e5245e") + }, + PythonDownload { + key: "cpython-3.9.18-windows-x86_64-none", + major: 3, + minor: 9, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.9.18%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("924ed4f375ef73c73a725ef18ec6a72726456673d5a116f132f60860a25dd674") + }, + PythonDownload { + key: "cpython-3.9.17-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("1f6c43d92ba9f4e15149cf5db6ecde11e05eee92c070a085e44f46c559520257") + }, + PythonDownload { + key: "cpython-3.9.17-macos-aarch64-none", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("2902e2a0add6d584999fa27896b721a359f7308404e936e80b01b07aa06e8f5e") + }, + PythonDownload { + key: "cpython-3.9.17-linux-powerpc64le-gnu", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("bcb0ec31342df52b4555be309080a9c3224e7ff60a6291e34337ddfddef111cf") + }, + PythonDownload { + key: "cpython-3.9.17-linux-s390x-gnu", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::S390X, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-s390x-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("bf8c846c1a4e52355d4ae294f4e1da9587d5415467eb6890bdf0f5a4c8cda396") + }, + PythonDownload { + key: "cpython-3.9.17-linux-x86-gnu", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("1a9b7edc16683410c27bc5b4b1761143bef7831a1ad172e7e3581c152c6837a2") + }, + PythonDownload { + key: "cpython-3.9.17-windows-x86-none", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("ffac27bfb8bdf615d0fc6cbbe0becaa65b6ae73feec417919601497fce2be0ab") + }, + PythonDownload { + key: "cpython-3.9.17-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("649fff6048f4cb9e64a85eaf8e720eb4c3257e27e7c4ee46f75bfa48c18c6826") + }, + PythonDownload { + key: "cpython-3.9.17-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("8496473a97e1dd43bf96fc1cf19f02f305608ef6a783e0112274e0ae01df4f2a") + }, + PythonDownload { + key: "cpython-3.9.17-macos-x86_64-none", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("ba04f9813b78b61d60a27857949403a1b1dd8ac053e1f1aff72fe2689c238d3c") + }, + PythonDownload { + key: "cpython-3.9.17-windows-x86_64-none", + major: 3, + minor: 9, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.9.17%2B20230726-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("209983b8227e4755197dfed4f6887e45b6a133f61e7eb913c0a934b0d0c3e00f") + }, + PythonDownload { + key: "cpython-3.9.16-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("57ac7ce9d3dd32c1277ee7295daf5ad7b5ecc929e65b31f11b1e7b94cd355ed1") + }, + PythonDownload { + key: "cpython-3.9.16-macos-aarch64-none", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("c86ed2bf3ff290af10f96183c53e2b29e954abb520806fbe01d3ef2f9d809a75") + }, + PythonDownload { + key: "cpython-3.9.16-linux-powerpc64le-gnu", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::Powerpc64Le, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-ppc64le-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("8b2e7ddc6feb116dfa6829cfc478be90a374dc5ce123a98bc77e86d0e93e917d") + }, + PythonDownload { + key: "cpython-3.9.16-linux-x86-gnu", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("e2a0226165550492e895369ee1b69a515f82e12cb969656012ee8e1543409661") + }, + PythonDownload { + key: "cpython-3.9.16-windows-x86-none", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("d7994b5febb375bb131d028f98f4902ba308913c77095457ccd159b521e20c52") + }, + PythonDownload { + key: "cpython-3.9.16-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("cdc1290b9bdb2f74a6c48ab24531919551128e39773365c6f3e17668216275a0") + }, + PythonDownload { + key: "cpython-3.9.16-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("c397f292021b33531248ad8fede24ef6249cc6172347b2017f92b4a71845b8ed") + }, + PythonDownload { + key: "cpython-3.9.16-macos-x86_64-none", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("5809626ca7907c8ea397341f3d5eafb280ed5b19cc5622e57b14d9b4362eba50") + }, + PythonDownload { + key: "cpython-3.9.16-windows-x86_64-none", + major: 3, + minor: 9, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230507/cpython-3.9.16%2B20230507-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("199c821505e287c004c3796ba9ac4bd129d7793e1d833e9a7672ed03bdb397d4") + }, + PythonDownload { + key: "cpython-3.9.15-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("0da1f081313b088c1381206e698e70fffdffc01e1b2ce284145c24ee5f5b4cbb") + }, + PythonDownload { + key: "cpython-3.9.15-macos-aarch64-none", + major: 3, + minor: 9, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("1799b97619572ad595cd6d309bbcc57606138a57f4e90af04e04ee31d187e22f") + }, + PythonDownload { + key: "cpython-3.9.15-linux-x86-gnu", + major: 3, + minor: 9, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("cbc6a14835022d89f4ca6042a06c4959d74d4bbb58e70bdbe0fe8d2928934922") + }, + PythonDownload { + key: "cpython-3.9.15-windows-x86-none", + major: 3, + minor: 9, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("a5ad2a6ace97d458ad7b2857fba519c5c332362442d88e2b23ed818f243b8a78") + }, + PythonDownload { + key: "cpython-3.9.15-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("6a08761bb725b8d3a92144f81628febeab8b12326ca264ffe28255fa67c7bf17") + }, + PythonDownload { + key: "cpython-3.9.15-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("4597f0009cfb52e748a57badab28edf84a263390b777c182b18c36d666a01440") + }, + PythonDownload { + key: "cpython-3.9.15-macos-x86_64-none", + major: 3, + minor: 9, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("50fd795eac55c4485e2fefbb8e7b365461817733c45becb50a7480a243e6000e") + }, + PythonDownload { + key: "cpython-3.9.15-windows-x86_64-none", + major: 3, + minor: 9, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.9.15%2B20221106-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("d0f3ce1748a51779eedf155aea617c39426e3f7bfd93b4876cb172576b6e8bda") + }, + PythonDownload { + key: "cpython-3.9.14-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("3020c743e4742d6e0e5d27fcb166c694bf1d9565369b2eaee9d68434304aebd2") + }, + PythonDownload { + key: "cpython-3.9.14-macos-aarch64-none", + major: 3, + minor: 9, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("6b9d2ff724aff88a4d0790c86f2e5d17037736f35a796e71732624191ddd6e38") + }, + PythonDownload { + key: "cpython-3.9.14-linux-x86-gnu", + major: 3, + minor: 9, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("83a11c4f3d1c0ec39119bd0513a8684b59b68c3989cf1e5042d7417d4770c904") + }, + PythonDownload { + key: "cpython-3.9.14-windows-x86-none", + major: 3, + minor: 9, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("fae990eb312314102408cb0c0453dae670f0eb468f4cbf3e72327ceaa1276b46") + }, + PythonDownload { + key: "cpython-3.9.14-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("54529c0a8ffe621f5c9c6bdd22968cac9d3207cbd5dcd9c07bbe61140c49937e") + }, + PythonDownload { + key: "cpython-3.9.14-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("5638c12d47eb81adf96615cea8a5a61e8414c3ac03a8b570d30ae9998cb6d030") + }, + PythonDownload { + key: "cpython-3.9.14-macos-x86_64-none", + major: 3, + minor: 9, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("186155e19b63da3248347415f888fbcf982c7587f6f927922ca243ae3f23ed2f") + }, + PythonDownload { + key: "cpython-3.9.14-windows-x86_64-none", + major: 3, + minor: 9, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.9.14%2B20221002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("49f27a3a18b4c2d765b0656c6529378a20b3e37fdb0aca9490576ff7a67243a9") + }, + PythonDownload { + key: "cpython-3.9.13-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("8c706ebb2c8970da4fbec95b0520b4632309bc6a3e115cf309e38f181b553d14") + }, + PythonDownload { + key: "cpython-3.9.13-macos-aarch64-none", + major: 3, + minor: 9, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("8612e9328663c0747d1eae36b218d11c2fbc53c39ec7512c7ad6b1b57374a5dc") + }, + PythonDownload { + key: "cpython-3.9.13-linux-x86-gnu", + major: 3, + minor: 9, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("7d33637b48c45acf8805d5460895dca29bf2740fd2cf502fde6c6a00637db6b5") + }, + PythonDownload { + key: "cpython-3.9.13-windows-x86-none", + major: 3, + minor: 9, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("3860abee418825c6a33f76fe88773fb05eb4bc724d246f1af063106d9ea3f999") + }, + PythonDownload { + key: "cpython-3.9.13-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("352d00a4630d0665387bcb158aec3f6c7fc5a4d14d65ac26e1b826e20611222f") + }, + PythonDownload { + key: "cpython-3.9.13-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("c7e48545a8291fe1be909c4454b5c48df0ee4e69e2b5e13b6144b4199c31f895") + }, + PythonDownload { + key: "cpython-3.9.13-macos-x86_64-none", + major: 3, + minor: 9, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("16d21a6e62c19c574a4a225961e80966449095a8eb2c4150905e30d4e807cf86") + }, + PythonDownload { + key: "cpython-3.9.13-windows-x86_64-none", + major: 3, + minor: 9, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.9.13%2B20220802-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("6ef2b164cae483c61da30fb6d245762b8d6d91346d66cb421989d6d1462e5a48") + }, + PythonDownload { + key: "cpython-3.9.12-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("202ef64e43570f0843ff5895fd9c1a2c36a96b48d52842fa95842d7d11025b20") + }, + PythonDownload { + key: "cpython-3.9.12-macos-aarch64-none", + major: 3, + minor: 9, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("b3d09b3c12295e893ee8f2cb60e8af94d8a21fc5c65016282925220f5270b85b") + }, + PythonDownload { + key: "cpython-3.9.12-linux-x86-gnu", + major: 3, + minor: 9, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("e52fdbe61dea847323cd6e81142d16a571dca9c0bcde3bfe5ae75a8d3d1a3bf4") + }, + PythonDownload { + key: "cpython-3.9.12-windows-x86-none", + major: 3, + minor: 9, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("361b8fa66d6b5d5623fd5e64af29cf220a693ba86d031bf7ce2b61e1ea50f568") + }, + PythonDownload { + key: "cpython-3.9.12-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("7290ac14e43749afdb37d3c9690f300f5f0786f19982e8960566ecdc3e42c3eb") + }, + PythonDownload { + key: "cpython-3.9.12-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("eb122ab2bf0b2d71926984bc7cf5fef65b415abfe01a0974ed6c1a2502fac764") + }, + PythonDownload { + key: "cpython-3.9.12-macos-x86_64-none", + major: 3, + minor: 9, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("825970ae30ae7a30a5b039aa25f1b965e2d1fe046e196e61fa2a3af8fef8c5d9") + }, + PythonDownload { + key: "cpython-3.9.12-windows-x86_64-none", + major: 3, + minor: 9, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220502/cpython-3.9.12%2B20220502-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("c49f8b07e9c4dcfd7a5b55c131e882a4ebdf9f37fef1c7820c3ce9eb23bab8ab") + }, + PythonDownload { + key: "cpython-3.9.11-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("e1f3ae07a28a687f8602fb4d29a1b72cc5e113c61dc6769d0d85081ab3e09c71") + }, + PythonDownload { + key: "cpython-3.9.11-macos-aarch64-none", + major: 3, + minor: 9, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("6d9f20607a20e2cc5ad1428f7366832dc68403fc15f2e4f195817187e7b6dbbf") + }, + PythonDownload { + key: "cpython-3.9.11-linux-x86-gnu", + major: 3, + minor: 9, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("0be0a5f524c68d521be2417565ca43f3125b1845f996d6d62266aa431e673f93") + }, + PythonDownload { + key: "cpython-3.9.11-windows-x86-none", + major: 3, + minor: 9, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("f06338422e7e3ad25d0cd61864bdb36d565d46440dd363cbb98821d388ed377a") + }, + PythonDownload { + key: "cpython-3.9.11-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("020bcbfff16dc5ce35a898763be3d847c97df2e14dabf483a8ec88b0455ff971") + }, + PythonDownload { + key: "cpython-3.9.11-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("d83eb5c897120e32287cb6fe5c24dd2dcae00878b3f9d7002590d468bd5de0f1") + }, + PythonDownload { + key: "cpython-3.9.11-macos-x86_64-none", + major: 3, + minor: 9, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("35e649618e7e602778e72b91c9c50c97d01a0c3509d16225a1f41dd0fd6575f0") + }, + PythonDownload { + key: "cpython-3.9.11-windows-x86_64-none", + major: 3, + minor: 9, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220318/cpython-3.9.11%2B20220318-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("1fe3c519d43737dc7743aec43f72735e1429c79e06e3901b21bad67b642f1a10") + }, + PythonDownload { + key: "cpython-3.9.10-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("8bf7ac2cd5825b8fde0a6e535266a57c97e82fd5a97877940920b403ca5e53d7") + }, + PythonDownload { + key: "cpython-3.9.10-macos-aarch64-none", + major: 3, + minor: 9, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("ba1b63600ed8d9f3b8d739657bd8e7f5ca167de29a1a58d04b2cd9940b289464") + }, + PythonDownload { + key: "cpython-3.9.10-linux-x86-gnu", + major: 3, + minor: 9, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("3e3bf4d3e71a2131e6c064d1e5019f58cb9c58fdceae4b76b26ac978a6d49aad") + }, + PythonDownload { + key: "cpython-3.9.10-windows-x86-none", + major: 3, + minor: 9, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("7f3ca15f89775f76a32e6ea9b2c9778ebf0cde753c5973d4493959e75dd92488") + }, + PythonDownload { + key: "cpython-3.9.10-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("d453abf741c3196ffc8470f3ea6404a3e2b55b2674a501bb79162f06122423e5") + }, + PythonDownload { + key: "cpython-3.9.10-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("2744b817f249c0563b844cddd5aba4cc2fd449489b8bd59980d7a31de3a4ece1") + }, + PythonDownload { + key: "cpython-3.9.10-macos-x86_64-none", + major: 3, + minor: 9, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("ef2f090ff920708b4b9aa5d6adf0dc930c09a4bf638d71e6883091f9e629193d") + }, + PythonDownload { + key: "cpython-3.9.10-windows-x86_64-none", + major: 3, + minor: 9, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.9.10%2B20220227-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("56b2738599131d03b39b914ea0597862fd9096e5e64816bf19466bf026e74f0c") + }, + PythonDownload { + key: "cpython-3.9.7-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-aarch64-unknown-linux-gnu-debug-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.7-macos-aarch64-none", + major: 3, + minor: 9, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-aarch64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.7-linux-x86-gnu", + major: 3, + minor: 9, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-i686-unknown-linux-gnu-debug-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.7-windows-x86-none", + major: 3, + minor: 9, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-i686-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.7-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-unknown-linux-gnu-debug-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.7-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-unknown-linux-musl-lto-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.7-macos-x86_64-none", + major: 3, + minor: 9, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-apple-darwin-pgo%2Blto-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.7-windows-x86_64-none", + major: 3, + minor: 9, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20211017/cpython-3.9.7-x86_64-pc-windows-msvc-shared-pgo-20211017T1616.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.6-linux-aarch64-gnu", + major: 3, + minor: 9, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-aarch64-unknown-linux-gnu-debug-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.6-macos-aarch64-none", + major: 3, + minor: 9, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-aarch64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.6-linux-x86-gnu", + major: 3, + minor: 9, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-i686-unknown-linux-gnu-debug-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.6-windows-x86-none", + major: 3, + minor: 9, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-i686-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.6-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-unknown-linux-gnu-debug-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.6-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-unknown-linux-musl-lto-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.6-macos-x86_64-none", + major: 3, + minor: 9, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.6-windows-x86_64-none", + major: 3, + minor: 9, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.9.6-x86_64-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.5-macos-aarch64-none", + major: 3, + minor: 9, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-aarch64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.5-linux-x86-gnu", + major: 3, + minor: 9, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-i686-unknown-linux-gnu-debug-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.5-windows-x86-none", + major: 3, + minor: 9, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-i686-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.5-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-unknown-linux-gnu-debug-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.5-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-unknown-linux-musl-lto-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.5-macos-x86_64-none", + major: 3, + minor: 9, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.5-windows-x86_64-none", + major: 3, + minor: 9, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.9.5-x86_64-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.4-macos-aarch64-none", + major: 3, + minor: 9, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-aarch64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.4-linux-x86-gnu", + major: 3, + minor: 9, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-i686-unknown-linux-gnu-debug-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.4-windows-x86-none", + major: 3, + minor: 9, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-i686-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.4-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-unknown-linux-gnu-debug-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.4-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-unknown-linux-musl-lto-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.4-macos-x86_64-none", + major: 3, + minor: 9, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.4-windows-x86_64-none", + major: 3, + minor: 9, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.9.4-x86_64-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.3-macos-aarch64-none", + major: 3, + minor: 9, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-aarch64-apple-darwin-pgo%2Blto-20210413T2055.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.3-windows-x86-none", + major: 3, + minor: 9, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-i686-pc-windows-msvc-shared-pgo-20210413T2055.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.3-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-unknown-linux-gnu-debug-20210413T2055.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.3-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-unknown-linux-musl-lto-20210413T2055.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.3-macos-x86_64-none", + major: 3, + minor: 9, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-apple-darwin-pgo%2Blto-20210413T2055.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.3-windows-x86_64-none", + major: 3, + minor: 9, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210414/cpython-3.9.3-x86_64-pc-windows-msvc-shared-pgo-20210413T2055.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.2-macos-aarch64-none", + major: 3, + minor: 9, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-aarch64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.2-linux-x86-gnu", + major: 3, + minor: 9, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-i686-unknown-linux-gnu-debug-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.2-windows-x86-none", + major: 3, + minor: 9, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-i686-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.2-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-unknown-linux-gnu-debug-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.2-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-unknown-linux-musl-lto-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.2-macos-x86_64-none", + major: 3, + minor: 9, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.2-windows-x86_64-none", + major: 3, + minor: 9, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.9.2-x86_64-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.1-windows-x86-none", + major: 3, + minor: 9, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-i686-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.1-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-unknown-linux-gnu-debug-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.1-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-unknown-linux-musl-debug-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.1-macos-x86_64-none", + major: 3, + minor: 9, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-apple-darwin-pgo-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.1-windows-x86_64-none", + major: 3, + minor: 9, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.9.1-x86_64-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.0-windows-x86-none", + major: 3, + minor: 9, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-i686-pc-windows-msvc-shared-pgo-20201021T0245.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.0-linux-x86_64-gnu", + major: 3, + minor: 9, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-unknown-linux-gnu-debug-20201020T0627.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.0-linux-x86_64-musl", + major: 3, + minor: 9, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-unknown-linux-musl-debug-20201020T0627.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.0-macos-x86_64-none", + major: 3, + minor: 9, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-apple-darwin-pgo-20201020T0626.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.9.0-windows-x86_64-none", + major: 3, + minor: 9, + patch: 0, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.9.0-x86_64-pc-windows-msvc-shared-pgo-20201021T0245.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.18-linux-aarch64-gnu", + major: 3, + minor: 8, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("6d71175a090950c2063680f250b8799ab39eb139aa1721c853d8950aadd1d4e2") + }, + PythonDownload { + key: "cpython-3.8.18-macos-aarch64-none", + major: 3, + minor: 8, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("c732c068cddcd6a008c1d6d8e35802f5bdc7323bd2eb64e77210d3d5fe4740c2") + }, + PythonDownload { + key: "cpython-3.8.18-windows-x86-none", + major: 3, + minor: 8, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("9f94c7b54b97116cd308e73cda0b7a7b7fff4515932c5cbba18eeae9ec798351") + }, + PythonDownload { + key: "cpython-3.8.18-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("189ae3b8249c57217e3253f9fc89857e088763cf2107a3f22ab2ac2398f41a65") + }, + PythonDownload { + key: "cpython-3.8.18-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("fa1bf64cf52d830e7b4bba486c447ee955af644d167df7c42afd169c5dc71d6a") + }, + PythonDownload { + key: "cpython-3.8.18-macos-x86_64-none", + major: 3, + minor: 8, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("4d4b65dd821ce13dcf6dfea3ad5c2d4c3d3a8c2b7dd49fc35c1d79f66238e89b") + }, + PythonDownload { + key: "cpython-3.8.18-windows-x86_64-none", + major: 3, + minor: 8, + patch: 18, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20240224/cpython-3.8.18%2B20240224-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("c63abd9365a13196eb9f65db864f95b85c1f90b770d218c1acd104e6b48a99d3") + }, + PythonDownload { + key: "cpython-3.8.17-linux-aarch64-gnu", + major: 3, + minor: 8, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("eaee5a0b79cc28943e19df54f314634795aee43a6670ce99c0306893a18fa784") + }, + PythonDownload { + key: "cpython-3.8.17-macos-aarch64-none", + major: 3, + minor: 8, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("d08a542bed35fc74ac6e8f6884c8aa29a77ff2f4ed04a06dcf91578dea622f9a") + }, + PythonDownload { + key: "cpython-3.8.17-linux-x86-gnu", + major: 3, + minor: 8, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("61ac08680c022f180a32dc82d84548aeb92c7194a489e3b3c532dc48f999d757") + }, + PythonDownload { + key: "cpython-3.8.17-windows-x86-none", + major: 3, + minor: 8, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("0931d8ca0e060c6ac1dfcf6bb9b6dea0ac3a9d95daf7906a88128045f4464bf8") + }, + PythonDownload { + key: "cpython-3.8.17-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("f499750ab0019f36ccb4d964e222051d0d49a1d1e8dbada98abae738cf48c9dc") + }, + PythonDownload { + key: "cpython-3.8.17-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("a316ba0b1f425b04c8dfd7a8a18a05d72ae5852732d401b16d7439bdf25caec3") + }, + PythonDownload { + key: "cpython-3.8.17-macos-x86_64-none", + major: 3, + minor: 8, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("2c4925f5cf37d498e0d8cfe7b10591cc5f0cd80d2582f566b12006e6f96958b1") + }, + PythonDownload { + key: "cpython-3.8.17-windows-x86_64-none", + major: 3, + minor: 8, + patch: 17, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230826/cpython-3.8.17%2B20230826-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("68c7d03de5283c4812f2706c797b2139999a28cec647bc662d1459a922059318") + }, + PythonDownload { + key: "cpython-3.8.16-linux-aarch64-gnu", + major: 3, + minor: 8, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("423d43d93e2fe33b41ad66d35426f16541f09fee9d7272ae5decf5474ebbc225") + }, + PythonDownload { + key: "cpython-3.8.16-macos-aarch64-none", + major: 3, + minor: 8, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("bfc91d0a1d6d6dfaa5a31c925aa6adae82bd1ae5eb17813a9f0a50bf9d3e6305") + }, + PythonDownload { + key: "cpython-3.8.16-linux-x86-gnu", + major: 3, + minor: 8, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("9aa3e559130a47c33ee2b67f6ca69e2f10d8f70c1fd1e2871763b892372a6d9e") + }, + PythonDownload { + key: "cpython-3.8.16-windows-x86-none", + major: 3, + minor: 8, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("5de953621402c11cc7db65ba15d45779e838d7ce78e7aa8d43c7d78fff177f13") + }, + PythonDownload { + key: "cpython-3.8.16-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("f12f5cb38f796ca48dc73262c05506a6f21f59d24e709ea0390b18bf71c2e1f9") + }, + PythonDownload { + key: "cpython-3.8.16-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("f7d46196b44d12a26209ac74061200aac478b96c253eea93a0b9734efa642779") + }, + PythonDownload { + key: "cpython-3.8.16-macos-x86_64-none", + major: 3, + minor: 8, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("21c0f4a0fa6ee518b9f2f1901c9667e3baf45d9f84235408b7ca50499d19f56d") + }, + PythonDownload { + key: "cpython-3.8.16-windows-x86_64-none", + major: 3, + minor: 8, + patch: 16, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20230726/cpython-3.8.16%2B20230726-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("6316713c2dcb30127b38ced249fa9608830a33459580b71275a935aaa8cd5d5f") + }, + PythonDownload { + key: "cpython-3.8.15-linux-aarch64-gnu", + major: 3, + minor: 8, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("2e80025eda686c14a9a0618ced40043c1d577a754b904fd7a382cd41abf9ca00") + }, + PythonDownload { + key: "cpython-3.8.15-macos-aarch64-none", + major: 3, + minor: 8, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("fc0f944e6f01ed649f79c873af1c317db61d2136b82081b4d7cbb7755f878035") + }, + PythonDownload { + key: "cpython-3.8.15-linux-x86-gnu", + major: 3, + minor: 8, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("b8436415ea9bd9970fb766f791a14b0e14ce6351fc4604eb158f1425e8bb4a33") + }, + PythonDownload { + key: "cpython-3.8.15-windows-x86-none", + major: 3, + minor: 8, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("98bb2315c3567316c30b060d613c8d6067b368b64f08ef8fe6196341637c1d78") + }, + PythonDownload { + key: "cpython-3.8.15-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("c3c8c23e34bddb4a2b90333ff17041f344401775d505700f1ceddb3ad9d589e0") + }, + PythonDownload { + key: "cpython-3.8.15-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("231b35d3c2cff0372d17cea7ff5168c0684a920b94a912ffc965c2518cacb694") + }, + PythonDownload { + key: "cpython-3.8.15-macos-x86_64-none", + major: 3, + minor: 8, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("e4fd2fa2255295fbdcfadb8b48014fa80810305eccb246d355880aabb45cbe93") + }, + PythonDownload { + key: "cpython-3.8.15-windows-x86_64-none", + major: 3, + minor: 8, + patch: 15, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221106/cpython-3.8.15%2B20221106-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("59beac5610e6da0848ebaccd72f91f6aaaeed65ef59606d006af909e9e79beba") + }, + PythonDownload { + key: "cpython-3.8.14-linux-aarch64-gnu", + major: 3, + minor: 8, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("a14d8b5cbd8e1ca45cbcb49f4bf0b0440dc86eb95b7c3da3c463a704a3b4593c") + }, + PythonDownload { + key: "cpython-3.8.14-macos-aarch64-none", + major: 3, + minor: 8, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("d17a3fcc161345efa2ec0b4ab9c9ed6c139d29128f2e34bb636338a484aa7b72") + }, + PythonDownload { + key: "cpython-3.8.14-linux-x86-gnu", + major: 3, + minor: 8, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("631bb90fe8f2965d03400b268de90fe155ce51961296360d6578b7151aa9ef4c") + }, + PythonDownload { + key: "cpython-3.8.14-windows-x86-none", + major: 3, + minor: 8, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("e43f7a5044eac91e95df59fd08bf96f13245898876fc2afd90a081cfcd847e35") + }, + PythonDownload { + key: "cpython-3.8.14-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("bd1e8e09edccaab82fbd75b457205a076847d62e3354c3d9b5abe985181047fc") + }, + PythonDownload { + key: "cpython-3.8.14-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("c6da442aaea160179a9379b297ccb3ba09b825fc27d84577fc28e62911451e7d") + }, + PythonDownload { + key: "cpython-3.8.14-macos-x86_64-none", + major: 3, + minor: 8, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("62edfea77b42e87ca2d85c482319211cd2dd68d55ba85c99f1834f7b64a60133") + }, + PythonDownload { + key: "cpython-3.8.14-windows-x86_64-none", + major: 3, + minor: 8, + patch: 14, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20221002/cpython-3.8.14%2B20221002-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("6986b3e6edf7b37f96ea940b7ccba7b767ed3ea9b3faec2a2a60e5b2c4443314") + }, + PythonDownload { + key: "cpython-3.8.13-linux-aarch64-gnu", + major: 3, + minor: 8, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-aarch64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("3a927205db4686c182b5e8f3fc7fd7d82ec8f61c70d5b2bfddd9673c7ddc07ba") + }, + PythonDownload { + key: "cpython-3.8.13-macos-aarch64-none", + major: 3, + minor: 8, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("a204e5f9e1566bdc170b163300a29fc9580d5c65cd6e896caf6500cd64471373") + }, + PythonDownload { + key: "cpython-3.8.13-linux-x86-gnu", + major: 3, + minor: 8, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("6daf0405beae6d127a2dcae61d51a719236b861b4cabc220727e48547fd6f045") + }, + PythonDownload { + key: "cpython-3.8.13-windows-x86-none", + major: 3, + minor: 8, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("5630739d1c6fcfbf90311d236c5e46314fc4b439364429bee12d0ffc95e134fb") + }, + PythonDownload { + key: "cpython-3.8.13-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("891b5d7b0e936b98a62f65bc0b28fff61ca9002125a2fc1ebb9c72f6b0056712") + }, + PythonDownload { + key: "cpython-3.8.13-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("410f3223021d1b439cf8e4da699f868adada2066e354d88a00b5f365dc66c4bf") + }, + PythonDownload { + key: "cpython-3.8.13-macos-x86_64-none", + major: 3, + minor: 8, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("f706a62de8582bf84b8b693c993314cd786f3e78639892cfd9a7283a526696f9") + }, + PythonDownload { + key: "cpython-3.8.13-windows-x86_64-none", + major: 3, + minor: 8, + patch: 13, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220802/cpython-3.8.13%2B20220802-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("c36b703b8b806a047ba71e5e85734ac78d204d3a2b7ebc2efcdc7d4af6f6c263") + }, + PythonDownload { + key: "cpython-3.8.12-macos-aarch64-none", + major: 3, + minor: 8, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::Aarch64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-aarch64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("386f667f8d49b6c34aee1910cdc0b5b41883f9406f98e7d59a3753990b1cdbac") + }, + PythonDownload { + key: "cpython-3.8.12-linux-x86-gnu", + major: 3, + minor: 8, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-i686-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("7cfac9a57e262be3e889036d7fc570293e6d3d74411ee23e1fa9aa470d387e6a") + }, + PythonDownload { + key: "cpython-3.8.12-windows-x86-none", + major: 3, + minor: 8, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-i686-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("3e2e6c7de78b1924aad37904fed7bfbac6efa2bef05348e9be92180b2f2b1ae1") + }, + PythonDownload { + key: "cpython-3.8.12-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-unknown-linux-gnu-debug-full.tar.zst", + sha256: Some("9ad20c520c291d08087e9afb4390f389d2b66c7fc97f23fffc1313ebafc5fee0") + }, + PythonDownload { + key: "cpython-3.8.12-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-unknown-linux-musl-lto-full.tar.zst", + sha256: Some("3d958e3f984637d8ca4a90a2e068737b268f87fc615121a6f1808cd46ccacc48") + }, + PythonDownload { + key: "cpython-3.8.12-macos-x86_64-none", + major: 3, + minor: 8, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-apple-darwin-pgo%2Blto-full.tar.zst", + sha256: Some("cf614d96e2001d526061b3ce0569c79057fd0074ace472ff4f5f601262e08cdb") + }, + PythonDownload { + key: "cpython-3.8.12-windows-x86_64-none", + major: 3, + minor: 8, + patch: 12, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20220227/cpython-3.8.12%2B20220227-x86_64-pc-windows-msvc-shared-pgo-full.tar.zst", + sha256: Some("33f278416ba8074f2ca6d7f8c17b311b60537c9e6431fd47948784c2a78ea227") + }, + PythonDownload { + key: "cpython-3.8.11-linux-x86-gnu", + major: 3, + minor: 8, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-i686-unknown-linux-gnu-debug-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.11-windows-x86-none", + major: 3, + minor: 8, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-i686-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.11-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-unknown-linux-gnu-debug-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.11-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-unknown-linux-musl-lto-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.11-macos-x86_64-none", + major: 3, + minor: 8, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-apple-darwin-pgo%2Blto-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.11-windows-x86_64-none", + major: 3, + minor: 8, + patch: 11, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210724/cpython-3.8.11-x86_64-pc-windows-msvc-shared-pgo-20210724T1424.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.10-linux-x86-gnu", + major: 3, + minor: 8, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-i686-unknown-linux-gnu-debug-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.10-windows-x86-none", + major: 3, + minor: 8, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-i686-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.10-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-unknown-linux-gnu-debug-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.10-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-unknown-linux-musl-lto-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.10-macos-x86_64-none", + major: 3, + minor: 8, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-apple-darwin-pgo%2Blto-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.10-windows-x86_64-none", + major: 3, + minor: 8, + patch: 10, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210506/cpython-3.8.10-x86_64-pc-windows-msvc-shared-pgo-20210506T0943.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.9-linux-x86-gnu", + major: 3, + minor: 8, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-i686-unknown-linux-gnu-debug-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.9-windows-x86-none", + major: 3, + minor: 8, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-i686-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.9-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-unknown-linux-gnu-debug-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.9-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-unknown-linux-musl-lto-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.9-macos-x86_64-none", + major: 3, + minor: 8, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-apple-darwin-pgo%2Blto-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.9-windows-x86_64-none", + major: 3, + minor: 8, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210415/cpython-3.8.9-x86_64-pc-windows-msvc-shared-pgo-20210414T1515.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.8-linux-x86-gnu", + major: 3, + minor: 8, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-i686-unknown-linux-gnu-debug-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.8-windows-x86-none", + major: 3, + minor: 8, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-i686-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.8-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-unknown-linux-gnu-debug-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.8-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-unknown-linux-musl-lto-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.8-macos-x86_64-none", + major: 3, + minor: 8, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-apple-darwin-pgo%2Blto-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.8-windows-x86_64-none", + major: 3, + minor: 8, + patch: 8, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210327/cpython-3.8.8-x86_64-pc-windows-msvc-shared-pgo-20210327T1202.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.7-windows-x86-none", + major: 3, + minor: 8, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-i686-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.7-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-unknown-linux-gnu-debug-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.7-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-unknown-linux-musl-debug-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.7-macos-x86_64-none", + major: 3, + minor: 8, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-apple-darwin-pgo-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.7-windows-x86_64-none", + major: 3, + minor: 8, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20210103/cpython-3.8.7-x86_64-pc-windows-msvc-shared-pgo-20210103T1125.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.6-windows-x86-none", + major: 3, + minor: 8, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-i686-pc-windows-msvc-shared-pgo-20201021T0233.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.6-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-unknown-linux-gnu-debug-20201020T0627.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.6-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-unknown-linux-musl-debug-20201020T0627.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.6-macos-x86_64-none", + major: 3, + minor: 8, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-apple-darwin-pgo-20201020T0626.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.6-windows-x86_64-none", + major: 3, + minor: 8, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20201020/cpython-3.8.6-x86_64-pc-windows-msvc-shared-pgo-20201021T0232.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.5-windows-x86-none", + major: 3, + minor: 8, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200830/cpython-3.8.5-i686-pc-windows-msvc-shared-pgo-20200830T2311.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.5-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.8.5-x86_64-unknown-linux-gnu-debug-20200823T0036.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.5-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.8.5-x86_64-unknown-linux-musl-debug-20200823T0036.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.5-macos-x86_64-none", + major: 3, + minor: 8, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.8.5-x86_64-apple-darwin-pgo-20200823T2228.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.5-windows-x86_64-none", + major: 3, + minor: 8, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200830/cpython-3.8.5-x86_64-pc-windows-msvc-shared-pgo-20200830T2254.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.3-windows-x86-none", + major: 3, + minor: 8, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-i686-pc-windows-msvc-shared-pgo-20200518T0154.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.3-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-x86_64-unknown-linux-gnu-debug-20200518T0040.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.3-linux-x86_64-musl", + major: 3, + minor: 8, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-x86_64-unknown-linux-musl-debug-20200518T0040.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.3-macos-x86_64-none", + major: 3, + minor: 8, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200530/cpython-3.8.3-x86_64-apple-darwin-pgo-20200530T1845.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.3-windows-x86_64-none", + major: 3, + minor: 8, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.8.3-x86_64-pc-windows-msvc-shared-pgo-20200517T2207.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.2-windows-x86-none", + major: 3, + minor: 8, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-i686-pc-windows-msvc-shared-pgo-20200418T2315.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.2-linux-x86_64-gnu", + major: 3, + minor: 8, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-unknown-linux-gnu-debug-20200418T2305.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.2-macos-x86_64-none", + major: 3, + minor: 8, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-apple-darwin-pgo-20200418T2238.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.8.2-windows-x86_64-none", + major: 3, + minor: 8, + patch: 2, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200418/cpython-3.8.2-x86_64-pc-windows-msvc-shared-pgo-20200418T2315.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.9-windows-x86-none", + major: 3, + minor: 7, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-i686-pc-windows-msvc-shared-pgo-20200823T0159.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.9-linux-x86_64-gnu", + major: 3, + minor: 7, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-x86_64-unknown-linux-gnu-debug-20200823T0036.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.9-linux-x86_64-musl", + major: 3, + minor: 7, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-x86_64-unknown-linux-musl-debug-20200823T0036.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.9-macos-x86_64-none", + major: 3, + minor: 7, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200823/cpython-3.7.9-x86_64-apple-darwin-pgo-20200823T2228.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.9-windows-x86_64-none", + major: 3, + minor: 7, + patch: 9, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200822/cpython-3.7.9-x86_64-pc-windows-msvc-shared-pgo-20200823T0118.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.7-windows-x86-none", + major: 3, + minor: 7, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-i686-pc-windows-msvc-shared-pgo-20200517T2153.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.7-linux-x86_64-gnu", + major: 3, + minor: 7, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-x86_64-unknown-linux-gnu-debug-20200518T0040.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.7-linux-x86_64-musl", + major: 3, + minor: 7, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-x86_64-unknown-linux-musl-debug-20200518T0040.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.7-macos-x86_64-none", + major: 3, + minor: 7, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200530/cpython-3.7.7-x86_64-apple-darwin-pgo-20200530T1845.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.7-windows-x86_64-none", + major: 3, + minor: 7, + patch: 7, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200517/cpython-3.7.7-x86_64-pc-windows-msvc-shared-pgo-20200517T2128.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.6-windows-x86-none", + major: 3, + minor: 7, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-windows-x86-shared-pgo-20200217T0110.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.6-linux-x86_64-gnu", + major: 3, + minor: 7, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-linux64-20200216T2303.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.6-linux-x86_64-musl", + major: 3, + minor: 7, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200217/cpython-3.7.6-linux64-musl-20200218T0557.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.6-macos-x86_64-none", + major: 3, + minor: 7, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-macos-20200216T2344.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.6-windows-x86_64-none", + major: 3, + minor: 7, + patch: 6, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20200216/cpython-3.7.6-windows-amd64-shared-pgo-20200217T0022.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.5-windows-x86-none", + major: 3, + minor: 7, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-windows-x86-20191025T0549.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.5-linux-x86_64-gnu", + major: 3, + minor: 7, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-linux64-20191025T0506.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.5-linux-x86_64-musl", + major: 3, + minor: 7, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-linux64-musl-20191026T0603.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.5-macos-x86_64-none", + major: 3, + minor: 7, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-macos-20191026T0535.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.5-windows-x86_64-none", + major: 3, + minor: 7, + patch: 5, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20191025/cpython-3.7.5-windows-amd64-20191025T0540.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.4-windows-x86-none", + major: 3, + minor: 7, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-windows-x86-20190817T0235.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.4-linux-x86_64-gnu", + major: 3, + minor: 7, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-linux64-20190817T0224.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.4-linux-x86_64-musl", + major: 3, + minor: 7, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-linux64-musl-20190817T0227.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.4-macos-x86_64-none", + major: 3, + minor: 7, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-macos-20190817T0220.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.4-windows-x86_64-none", + major: 3, + minor: 7, + patch: 4, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190816/cpython-3.7.4-windows-amd64-20190817T0227.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.3-windows-x86-none", + major: 3, + minor: 7, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-windows-x86-20190709T0348.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.3-linux-x86_64-gnu", + major: 3, + minor: 7, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-linux64-20190618T0324.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.3-linux-x86_64-musl", + major: 3, + minor: 7, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Musl, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-linux64-musl-20190618T0400.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.3-macos-x86_64-none", + major: 3, + minor: 7, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Macos, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-macos-20190618T0523.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.3-windows-x86_64-none", + major: 3, + minor: 7, + patch: 3, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Windows, + libc: Libc::None, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20190617/cpython-3.7.3-windows-amd64-20190618T0516.tar.zst", + sha256: None + }, + PythonDownload { + key: "cpython-3.7.1-linux-x86_64-gnu", + major: 3, + minor: 7, + patch: 1, + implementation: ImplementationName::Cpython, + arch: Arch::X86_64, + os: Os::Linux, + libc: Libc::Gnu, + url: "https://github.com/indygreg/python-build-standalone/releases/download/20181218/cpython-3.7.1-linux64-20181218T1905.tar.zst", + sha256: None + }, +]; diff --git a/crates/uv-toolchain/src/python_versions.inc.mustache b/crates/uv-toolchain/src/python_versions.inc.mustache new file mode 100644 index 000000000..9c81e82b3 --- /dev/null +++ b/crates/uv-toolchain/src/python_versions.inc.mustache @@ -0,0 +1,26 @@ +// DO NOT EDIT +// +// Generated with `{{generated_with}}` +// From template at `{{generated_from}}` + +pub(crate) const PYTHON_DOWNLOADS: &[PythonDownload] = &[ + {{#versions}} + PythonDownload { + key: "{{key}}", + major: {{value.major}}, + minor: {{value.minor}}, + patch: {{value.patch}}, + implementation: ImplementationName::{{value.name}}, + arch: Arch::{{value.arch}}, + os: Os::{{value.os}}, + libc: Libc::{{value.libc}}, + url: "{{value.url}}", + {{#value.sha256}} + sha256: Some("{{.}}") + {{/value.sha256}} + {{^value.sha256}} + sha256: None + {{/value.sha256}} + }, + {{/versions}} +]; diff --git a/crates/uv-toolchain/template-version-metadata.py b/crates/uv-toolchain/template-version-metadata.py new file mode 100644 index 000000000..f8d70f14d --- /dev/null +++ b/crates/uv-toolchain/template-version-metadata.py @@ -0,0 +1,99 @@ +#!/usr/bin/env python3.12 +""" +Generate static Rust code from Python version metadata. + +Generates the `python_versions.rs` file from the `python_versions.rs.mustache` template. + +Usage: + + python template-version-metadata.py +""" + +import sys +import logging +import argparse +import json +import subprocess +from pathlib import Path + +CRATE_ROOT = Path(__file__).parent +WORKSPACE_ROOT = CRATE_ROOT.parent.parent +VERSION_METADATA = CRATE_ROOT / "python-version-metadata.json" +TEMPLATE = CRATE_ROOT / "src" / "python_versions.inc.mustache" +TARGET = TEMPLATE.with_suffix("") + + +try: + import chevron_blue +except ImportError: + print( + "missing requirement `chevron-blue`", + file=sys.stderr, + ) + exit(1) + + +def prepare_value(value: dict) -> dict: + # Convert fields from snake case to camel case for enums + for key in ["arch", "os", "libc", "name"]: + value[key] = value[key].title() + return value + + +def main(): + debug = logging.getLogger().getEffectiveLevel() <= logging.DEBUG + + data = {} + data["generated_with"] = Path(__file__).relative_to(WORKSPACE_ROOT) + data["generated_from"] = TEMPLATE.relative_to(WORKSPACE_ROOT) + data["versions"] = [ + {"key": key, "value": prepare_value(value)} + for key, value in json.loads(VERSION_METADATA.read_text()).items() + ] + + # Render the template + logging.info(f"Rendering `{TEMPLATE.name}`...") + output = chevron_blue.render( + template=TEMPLATE.read_text(), data=data, no_escape=True, warn=debug + ) + + # Update the file + logging.info(f"Updating `{TARGET}`...") + TARGET.write_text(output) + subprocess.check_call( + ["rustfmt", str(TARGET)], + stderr=subprocess.STDOUT, + stdout=sys.stderr if debug else subprocess.DEVNULL, + ) + + logging.info("Done!") + + +if __name__ == "__main__": + parser = argparse.ArgumentParser( + description="Generates Rust code for Python version metadata.", + ) + parser.add_argument( + "-v", + "--verbose", + action="store_true", + help="Enable debug logging", + ) + parser.add_argument( + "-q", + "--quiet", + action="store_true", + help="Disable logging", + ) + + args = parser.parse_args() + if args.quiet: + log_level = logging.CRITICAL + elif args.verbose: + log_level = logging.DEBUG + else: + log_level = logging.INFO + + logging.basicConfig(level=log_level, format="%(message)s") + + main() diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 35db81fef..b34b9757b 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -34,6 +34,7 @@ uv-resolver = { workspace = true, features = ["clap"] } uv-types = { workspace = true, features = ["clap"] } uv-configuration = { workspace = true, features = ["clap"] } uv-virtualenv = { workspace = true } +uv-toolchain = { workspace = true } uv-warnings = { workspace = true } anstream = { workspace = true } diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index b48d33e60..032586d94 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -29,7 +29,7 @@ use uv_configuration::{ use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_installer::Downloader; -use uv_interpreter::{find_best_python, PythonEnvironment, PythonVersion}; +use uv_interpreter::{find_best_python, PythonEnvironment}; use uv_normalize::{ExtraName, PackageName}; use uv_requirements::{ upgrade::read_lockfile, ExtrasSpecification, LookaheadResolver, NamedRequirementsResolver, @@ -39,6 +39,7 @@ use uv_resolver::{ AnnotationStyle, DependencyMode, DisplayResolutionGraph, Exclusions, InMemoryIndex, Manifest, OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, }; +use uv_toolchain::PythonVersion; use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight}; use uv_warnings::warn_user; diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 5b6ec7ebf..0e162184f 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -21,10 +21,10 @@ use uv_configuration::{ Upgrade, }; use uv_configuration::{IndexStrategy, NoBinary}; -use uv_interpreter::PythonVersion; use uv_normalize::{ExtraName, PackageName}; use uv_requirements::{ExtrasSpecification, RequirementsSource}; use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode}; +use uv_toolchain::PythonVersion; use crate::commands::{extra_name_with_clap_error, ExitStatus, ListFormat, VersionFormat}; use crate::compat::CompatArgs; diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 251331a83..2b77cc9c5 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -4,11 +4,8 @@ use assert_cmd::assert::{Assert, OutputAssertExt}; use assert_cmd::Command; use assert_fs::assert::PathAssert; + use assert_fs::fixture::PathChild; -#[cfg(unix)] -use fs_err::os::unix::fs::symlink as symlink_file; -#[cfg(windows)] -use fs_err::os::windows::fs::symlink_file; use regex::Regex; use std::borrow::BorrowMut; use std::env; @@ -16,10 +13,11 @@ use std::ffi::OsString; use std::path::{Path, PathBuf}; use std::process::Output; use std::str::FromStr; -use uv_fs::Simplified; +use uv_interpreter::find_requested_python; use uv_cache::Cache; -use uv_interpreter::{find_requested_python, PythonVersion}; +use uv_fs::Simplified; +use uv_toolchain::{toolchains_for_version, PythonVersion}; // Exclude any packages uploaded after this date. pub static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z"; @@ -316,81 +314,23 @@ pub fn venv_to_interpreter(venv: &Path) -> PathBuf { } } -/// If bootstrapped python build standalone pythons exists in `/bin`, -/// return the paths to the directories containing the python binaries (i.e. as paths that -/// `which::which_in` can use). -/// -/// Use `scripts/bootstrap/install.py` to bootstrap. -/// -/// Python versions are sorted from newest to oldest. -pub fn bootstrapped_pythons() -> Option> { - // Current dir is `/crates/uv`. - let project_root = std::env::current_dir() - .unwrap() - .parent() - .unwrap() - .parent() - .unwrap() - .to_path_buf(); - let bootstrap_dir = if let Some(bootstrap_dir) = env::var_os("UV_BOOTSTRAP_DIR") { - let bootstrap_dir = PathBuf::from(bootstrap_dir); - if bootstrap_dir.is_absolute() { - bootstrap_dir - } else { - // cargo test changes directory to the test crate, but doesn't tell us from where the user is running the - // tests. We'll assume that it's the project root. - project_root.join(bootstrap_dir) - } - } else { - project_root.join("bin") - }; - let bootstrapped_pythons = bootstrap_dir.join("versions"); - let Ok(bootstrapped_pythons) = fs_err::read_dir(bootstrapped_pythons) else { - return None; - }; - - let mut bootstrapped_pythons: Vec = bootstrapped_pythons - .map(Result::unwrap) - .filter(|entry| entry.metadata().unwrap().is_dir()) - .map(|entry| { - if cfg!(unix) { - entry.path().join("install").join("bin") - } else if cfg!(windows) { - entry.path().join("install") - } else { - unimplemented!("Only Windows and Unix are supported") - } - }) - .collect(); - bootstrapped_pythons.sort(); - // Prefer the most recent patch version. - bootstrapped_pythons.reverse(); - Some(bootstrapped_pythons) -} - /// Create a virtual environment named `.venv` in a temporary directory with the given -/// Python version. Expected format for `python` is "python". +/// Python version. Expected format for `python` is "". pub fn create_venv>( temp_dir: &Parent, cache_dir: &assert_fs::TempDir, python: &str, ) -> PathBuf { - let python = if let Some(bootstrapped_pythons) = bootstrapped_pythons() { - bootstrapped_pythons - .into_iter() - // Good enough since we control the directory - .find(|path| path.to_str().unwrap().contains(&format!("@{python}"))) - .expect("Missing python bootstrap version") - .join(if cfg!(unix) { - "python3" - } else if cfg!(windows) { - "python.exe" - } else { - unimplemented!("Only Windows and Unix are supported") - }) - } else { - PathBuf::from(python) - }; + let python = toolchains_for_version( + &PythonVersion::from_str(python).expect("Tests should use a valid Python version"), + ) + .expect("Tests are run on a supported platform") + .first() + .map(uv_toolchain::Toolchain::executable) + // We'll search for the request Python on the PATH if not found in the toolchain versions + // We hack this into a `PathBuf` to satisfy the compiler but it's just a string + .unwrap_or(PathBuf::from(python)); + let venv = temp_dir.child(".venv"); Command::new(get_bin()) .arg("venv") @@ -414,34 +354,48 @@ pub fn get_bin() -> PathBuf { } /// Create a `PATH` with the requested Python versions available in order. -pub fn create_bin_with_executables( +/// +/// Generally this should be used with `UV_TEST_PYTHON_PATH`. +pub fn python_path_with_versions( temp_dir: &assert_fs::TempDir, python_versions: &[&str], ) -> anyhow::Result { - if let Some(bootstrapped_pythons) = bootstrapped_pythons() { - let selected_pythons = python_versions.iter().flat_map(|python_version| { - bootstrapped_pythons.iter().filter(move |path| { - // Good enough since we control the directory - path.to_str() - .unwrap() - .contains(&format!("@{python_version}")) + let cache = Cache::from_path(temp_dir.child("cache").to_path_buf())?; + let selected_pythons = python_versions + .iter() + .flat_map(|python_version| { + let inner = toolchains_for_version( + &PythonVersion::from_str(python_version) + .expect("Tests should use a valid Python version"), + ) + .expect("Tests are run on a supported platform") + .iter() + .map(|toolchain| { + toolchain + .executable() + .parent() + .expect("Executables must exist in a directory") + .to_path_buf() }) - }); - return Ok(env::join_paths(selected_pythons)?); - } + .collect::>(); + if inner.is_empty() { + // Fallback to a system lookup if we failed to find one in the toolchain directory + if let Some(interpreter) = find_requested_python(python_version, &cache).unwrap() { + vec![interpreter + .sys_executable() + .parent() + .expect("Python executable should always be in a directory") + .to_path_buf()] + } else { + panic!("Could not find Python {python_version} for test"); + } + } else { + inner + } + }) + .collect::>(); - let bin = temp_dir.child("bin"); - fs_err::create_dir(&bin)?; - for &request in python_versions { - let interpreter = find_requested_python(request, &Cache::temp().unwrap())? - .ok_or(uv_interpreter::Error::NoSuchPython(request.to_string()))?; - let name = interpreter - .sys_executable() - .file_name() - .expect("Discovered executable must have a filename"); - symlink_file(interpreter.sys_executable(), bin.child(name))?; - } - Ok(bin.canonicalize()?.into()) + Ok(env::join_paths(selected_pythons)?) } /// Execute the command and format its output status, stdout and stderr into a snapshot string. diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index 91d563ebf..780c5fd6d 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -13,14 +13,14 @@ use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::{FileWriteStr, PathChild}; use predicates::prelude::predicate; -use common::{create_bin_with_executables, get_bin, uv_snapshot, TestContext}; +use common::{get_bin, python_path_with_versions, uv_snapshot, TestContext}; mod common; /// Provision python binaries and return a `pip compile` command with options shared across all scenarios. fn command(context: &TestContext, python_versions: &[&str]) -> Command { - let bin = create_bin_with_executables(&context.temp_dir, python_versions) - .expect("Failed to create bin dir"); + let python_path = python_path_with_versions(&context.temp_dir, python_versions) + .expect("Failed to create Python test path"); let mut command = Command::new(get_bin()); command .arg("pip") @@ -34,7 +34,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command { .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) + .env("UV_TEST_PYTHON_PATH", python_path) .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 202d3a9c6..74ba1bf3a 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -13,7 +13,7 @@ use indoc::indoc; use predicates::Predicate; use url::Url; -use common::{create_bin_with_executables, create_venv, uv_snapshot, venv_to_interpreter}; +use common::{create_venv, python_path_with_versions, uv_snapshot, venv_to_interpreter}; use uv_fs::Simplified; use crate::common::{copy_dir_all, get_bin, TestContext}; @@ -338,8 +338,8 @@ fn link() -> Result<()> { .success(); let venv2 = context.temp_dir.child(".venv2"); - let bin = create_bin_with_executables(&context.temp_dir, &["3.12"]) - .expect("Failed to create bin dir"); + let python_path = python_path_with_versions(&context.temp_dir, &["3.12"]) + .expect("Failed to create Python test path"); Command::new(get_bin()) .arg("venv") .arg(venv2.as_os_str()) @@ -347,7 +347,7 @@ fn link() -> Result<()> { .arg(context.cache_dir.path()) .arg("--python") .arg("3.12") - .env("UV_TEST_PYTHON_PATH", bin) + .env("UV_TEST_PYTHON_PATH", python_path) .current_dir(&context.temp_dir) .assert() .success(); diff --git a/crates/uv/tests/venv.rs b/crates/uv/tests/venv.rs index c30c4e00b..26e098300 100644 --- a/crates/uv/tests/venv.rs +++ b/crates/uv/tests/venv.rs @@ -9,11 +9,9 @@ use assert_fs::fixture::ChildPath; use assert_fs::prelude::*; use fs_err::PathExt; use uv_fs::Simplified; -use uv_interpreter::PythonVersion; +use uv_toolchain::PythonVersion; -use crate::common::{ - create_bin_with_executables, get_bin, uv_snapshot, TestContext, EXCLUDE_NEWER, -}; +use crate::common::{get_bin, python_path_with_versions, uv_snapshot, TestContext, EXCLUDE_NEWER}; mod common; @@ -21,15 +19,15 @@ struct VenvTestContext { cache_dir: assert_fs::TempDir, temp_dir: assert_fs::TempDir, venv: ChildPath, - bin: OsString, + python_path: OsString, python_versions: Vec, } impl VenvTestContext { fn new(python_versions: &[&str]) -> Self { let temp_dir = assert_fs::TempDir::new().unwrap(); - let bin = create_bin_with_executables(&temp_dir, python_versions) - .expect("Failed to create bin dir"); + let python_path = python_path_with_versions(&temp_dir, python_versions) + .expect("Failed to create Python test path"); let venv = temp_dir.child(".venv"); let python_versions = python_versions .iter() @@ -41,7 +39,7 @@ impl VenvTestContext { cache_dir: assert_fs::TempDir::new().unwrap(), temp_dir, venv, - bin, + python_path, python_versions, } } @@ -54,7 +52,7 @@ impl VenvTestContext { .arg(self.cache_dir.path()) .arg("--exclude-newer") .arg(EXCLUDE_NEWER) - .env("UV_TEST_PYTHON_PATH", self.bin.clone()) + .env("UV_TEST_PYTHON_PATH", self.python_path.clone()) .current_dir(self.temp_dir.path()); command } @@ -397,9 +395,9 @@ fn windows_shims() -> Result<()> { let context = VenvTestContext::new(&["3.9", "3.8"]); let shim_path = context.temp_dir.child("shim"); - let py38 = std::env::split_paths(&context.bin) + let py38 = std::env::split_paths(&context.python_path) .last() - .expect("create_bin_with_executables to set up the python versions"); + .expect("python_path_with_versions to set up the python versions"); // We want 3.8 and the first version should be 3.9. // Picking the last is necessary to prove that shims work because the python version selects // the python version from the first path segment by default, so we take the last to prove it's not @@ -417,7 +415,7 @@ fn windows_shims() -> Result<()> { uv_snapshot!(context.filters(), context.venv_command() .arg(context.venv.as_os_str()) .arg("--clear") - .env("UV_TEST_PYTHON_PATH", format!("{};{}", shim_path.display(), context.bin.simplified_display())), @r###" + .env("UV_TEST_PYTHON_PATH", format!("{};{}", shim_path.display(), context.python_path.simplified_display())), @r###" success: true exit_code: 0 ----- stdout ----- diff --git a/scripts/bootstrap/install.py b/scripts/bootstrap/install.py deleted file mode 100755 index ce7cc1460..000000000 --- a/scripts/bootstrap/install.py +++ /dev/null @@ -1,204 +0,0 @@ -#!/usr/bin/env python3 -# /// script -# requires-python = ">=3.11" -# dependencies = [ -# "zstandard==0.22.0", -# ] -# /// -# -# Download required Python versions and install to `bin` -# Uses prebuilt Python distributions from indygreg/python-build-standalone -# -# This script can be run without Python installed via `install.sh` -# -# Requirements -# -# pip install zstandard==0.22.0 -# -# Usage -# -# python scripts/bootstrap/install.py -# -# Or -# -# pipx run scripts/bootstrap/install.py -# -# The Python versions are installed from `.python_versions`. -# Python versions are linked in-order such that the _last_ defined version will be the default. -# -# Version metadata can be updated with `fetch-version-metadata.py` - -import concurrent.futures -import hashlib -import json -import os -import platform -import shutil -import sys -import sysconfig -import tarfile -import tempfile -import urllib.parse -import urllib.request -from pathlib import Path - -try: - import zstandard -except ImportError: - print("ERROR: zstandard is required; install with `pip install zstandard==0.22.0`") - sys.exit(1) - -# Setup some file paths -THIS_DIR = Path(__file__).parent -ROOT_DIR = THIS_DIR.parent.parent -if bin_dir := os.environ.get("UV_BOOTSTRAP_DIR"): - BIN_DIR = Path(bin_dir) -else: - BIN_DIR = ROOT_DIR / "bin" -INSTALL_DIR = BIN_DIR / "versions" -VERSIONS_FILE = ROOT_DIR / ".python-versions" -VERSIONS_METADATA_FILE = THIS_DIR / "versions.json" - -# Map system information to those in the versions metadata -ARCH_MAP = {"aarch64": "arm64", "amd64": "x86_64"} -PLATFORM_MAP = {"win32": "windows"} -PLATFORM = sys.platform -ARCH = platform.machine().lower() -INTERPRETER = "cpython" - - -def decompress_file(archive_path: Path, output_path: Path): - if str(archive_path).endswith(".tar.zst"): - dctx = zstandard.ZstdDecompressor() - - with tempfile.TemporaryFile(suffix=".tar") as ofh: - with archive_path.open("rb") as ifh: - dctx.copy_stream(ifh, ofh) - ofh.seek(0) - with tarfile.open(fileobj=ofh) as z: - z.extractall(output_path) - else: - raise ValueError(f"Unknown archive type {archive_path.suffix}") - - -def sha256_file(path: Path): - h = hashlib.sha256() - - with open(path, "rb") as file: - while True: - # Reading is buffered, so we can read smaller chunks. - chunk = file.read(h.block_size) - if not chunk: - break - h.update(chunk) - - return h.hexdigest() - - -versions_metadata = json.loads(VERSIONS_METADATA_FILE.read_text()) -versions = VERSIONS_FILE.read_text().splitlines() - - -def get_key(version): - if platform.system() == "Linux": - libc = sysconfig.get_config_var("SOABI").split("-")[-1] - else: - libc = "none" - key = f"{INTERPRETER}-{version}-{PLATFORM_MAP.get(PLATFORM, PLATFORM)}-{ARCH_MAP.get(ARCH, ARCH)}-{libc}" - return key - - -def download(version): - key = get_key(version) - install_dir = INSTALL_DIR / f"{INTERPRETER}@{version}" - print(f"Downloading {key}") - - url = versions_metadata[key]["url"] - - if not url: - print(f"No matching download for {key}") - sys.exit(1) - - filename = url.split("/")[-1] - print(f"Downloading {urllib.parse.unquote(filename)}") - download_path = THIS_DIR / filename - with urllib.request.urlopen(url) as response: - with download_path.open("wb") as download_file: - shutil.copyfileobj(response, download_file) - - sha = versions_metadata[key]["sha256"] - if not sha: - print(f"WARNING: no checksum for {key}") - else: - print("Verifying checksum...", end="") - if sha256_file(download_path) != sha: - print(" FAILED!") - sys.exit(1) - print(" OK") - - if install_dir.exists(): - shutil.rmtree(install_dir) - print("Extracting to", install_dir) - install_dir.parent.mkdir(parents=True, exist_ok=True) - - # n.b. do not use `.with_suffix` as it will replace the patch Python version - extract_dir = Path(str(install_dir) + ".tmp") - - decompress_file(THIS_DIR / filename, extract_dir) - (extract_dir / "python").rename(install_dir) - (THIS_DIR / filename).unlink() - extract_dir.rmdir() - - return install_dir - - -def install(version, install_dir): - key = get_key(version) - - if PLATFORM == "win32": - executable = install_dir / "install" / "python.exe" - else: - # Use relative paths for links so if the bin is moved they don't break - executable = ( - "." / install_dir.relative_to(BIN_DIR) / "install" / "bin" / "python3" - ) - - major = versions_metadata[key]["major"] - minor = versions_metadata[key]["minor"] - - # Link as all version tuples, later versions in the file will take precedence - BIN_DIR.mkdir(parents=True, exist_ok=True) - - targets = [ - (BIN_DIR / f"python{version}"), - (BIN_DIR / f"python{major}.{minor}"), - (BIN_DIR / f"python{major}"), - (BIN_DIR / "python"), - ] - for target in targets: - target.unlink(missing_ok=True) - if PLATFORM == "win32": - target.hardlink_to(executable) - else: - target.symlink_to(executable) - - print(f"Installed executables for python{version}") - - -if __name__ == "__main__": - if INSTALL_DIR.exists(): - print("Removing existing installations...") - shutil.rmtree(INSTALL_DIR) - - # Download in parallel - with concurrent.futures.ProcessPoolExecutor(max_workers=len(versions)) as executor: - futures = [ - (version, executor.submit(download, version)) for version in versions - ] - - # Install sequentially so overrides are respected - for version, future in futures: - install_dir = future.result() - install(version, install_dir) - - print("Done!") diff --git a/scripts/scenarios/templates/compile.mustache b/scripts/scenarios/templates/compile.mustache index 1e019b656..7084da6be 100644 --- a/scripts/scenarios/templates/compile.mustache +++ b/scripts/scenarios/templates/compile.mustache @@ -13,14 +13,14 @@ use assert_cmd::assert::OutputAssertExt; use assert_fs::fixture::{FileWriteStr, PathChild}; use predicates::prelude::predicate; -use common::{create_bin_with_executables, get_bin, uv_snapshot, TestContext}; +use common::{python_path_with_versions, get_bin, uv_snapshot, TestContext}; mod common; /// Provision python binaries and return a `pip compile` command with options shared across all scenarios. fn command(context: &TestContext, python_versions: &[&str]) -> Command { - let bin = create_bin_with_executables(&context.temp_dir, python_versions) - .expect("Failed to create bin dir"); + let python_path = python_path_with_versions(&context.temp_dir, python_versions) + .expect("Failed to create Python test path"); let mut command = Command::new(get_bin()); command .arg("pip") @@ -34,7 +34,7 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command { .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) .env("UV_NO_WRAP", "1") - .env("UV_TEST_PYTHON_PATH", bin) + .env("UV_TEST_PYTHON_PATH", python_path) .current_dir(&context.temp_dir); if cfg!(all(windows, debug_assertions)) { From 520cd4689bbf591f178399f89b8a05b193e43c8c Mon Sep 17 00:00:00 2001 From: Zanie Blue Date: Wed, 10 Apr 2024 11:51:40 -0500 Subject: [PATCH 086/110] Add ecosystem test for flask (#2971) Alternative to: - https://github.com/astral-sh/uv/pull/2967 - #2946 Both of those are big and fail. I'd like to generalize this coverage though. --- .github/workflows/ci.yml | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 221bdacb7..c4e0e628c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -214,35 +214,41 @@ jobs: path: ./target/debug/uv.exe retention-days: 1 - ecosystem-test-prefect: + ecosystem-test: needs: build-binary-linux - name: "ecosystem test | prefect" + name: "ecosystem test | ${{ matrix.repo }}" runs-on: ubuntu-latest + strategy: + matrix: + include: + - repo: "prefecthq/prefect" + command: "uv pip install -e '.[dev]'" + python: "3.9" + - repo: "pallets/flask" + command: "uv pip install -r requirements/dev.txt" + python: "3.12" + fail-fast: false steps: - uses: actions/checkout@v4 + with: + repository: ${{ matrix.repo }} - uses: actions/setup-python@v5 with: - python-version: "3.9" + python-version: ${{ matrix.python }} - name: "Download binary" uses: actions/download-artifact@v4 with: name: uv-linux-${{ github.sha }} - - name: "Clone project" - run: gh repo clone prefecthq/prefect - env: - GH_TOKEN: ${{ github.token }} - - name: "Prepare binary" run: chmod +x ./uv - name: "Test" run: | - cd prefect - ../uv venv - ../uv pip install --upgrade -e '.[dev]' + ./uv venv + ./${{ matrix.command }} cache-test-ubuntu: needs: build-binary-linux From a9d554fa90a7c0e9fc7ef17319ce79f2fc0cfe2f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 14:07:03 -0400 Subject: [PATCH 087/110] Add a `--require-hashes` command-line setting (#2824) ## Summary I'll likely only merge this once the PR chain is further along, but this PR wires up the setting fro the CLI. --- crates/uv-resolver/src/options.rs | 10 +++++++++ crates/uv/src/commands/pip_install.rs | 7 +++++++ crates/uv/src/commands/pip_sync.rs | 6 ++++++ crates/uv/src/main.rs | 30 +++++++++++++++++++++++++++ 4 files changed, 53 insertions(+) diff --git a/crates/uv-resolver/src/options.rs b/crates/uv-resolver/src/options.rs index 55e6e5f07..764728391 100644 --- a/crates/uv-resolver/src/options.rs +++ b/crates/uv-resolver/src/options.rs @@ -9,6 +9,7 @@ pub struct Options { pub prerelease_mode: PreReleaseMode, pub dependency_mode: DependencyMode, pub exclude_newer: Option>, + pub require_hashes: bool, } /// Builder for [`Options`]. @@ -18,6 +19,7 @@ pub struct OptionsBuilder { prerelease_mode: PreReleaseMode, dependency_mode: DependencyMode, exclude_newer: Option>, + require_hashes: bool, } impl OptionsBuilder { @@ -54,6 +56,13 @@ impl OptionsBuilder { self } + /// Sets the `--requires-hash` flag. + #[must_use] + pub fn require_hashes(mut self, require_hashes: bool) -> Self { + self.require_hashes = require_hashes; + self + } + /// Builds the options. pub fn build(self) -> Options { Options { @@ -61,6 +70,7 @@ impl OptionsBuilder { prerelease_mode: self.prerelease_mode, dependency_mode: self.dependency_mode, exclude_newer: self.exclude_newer, + require_hashes: self.require_hashes, } } } diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index bc002de4a..280c0c0b7 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -67,6 +67,7 @@ pub(crate) async fn pip_install( reinstall: Reinstall, link_mode: LinkMode, compile: bool, + require_hashes: bool, setup_py: SetupPyStrategy, connectivity: Connectivity, config_settings: &ConfigSettings, @@ -84,6 +85,11 @@ pub(crate) async fn pip_install( printer: Printer, ) -> Result { let start = std::time::Instant::now(); + + if require_hashes { + warn_user!("Hash-checking mode (via `--require-hashes`) is not yet supported."); + } + let client_builder = BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls) @@ -292,6 +298,7 @@ pub(crate) async fn pip_install( .prerelease_mode(prerelease_mode) .dependency_mode(dependency_mode) .exclude_newer(exclude_newer) + .require_hashes(require_hashes) .build(); // Resolve the requirements. diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 5229613b7..b5d991ad1 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -45,6 +45,7 @@ pub(crate) async fn pip_sync( reinstall: &Reinstall, link_mode: LinkMode, compile: bool, + require_hashes: bool, index_locations: IndexLocations, index_strategy: IndexStrategy, keyring_provider: KeyringProvider, @@ -64,6 +65,10 @@ pub(crate) async fn pip_sync( ) -> Result { let start = std::time::Instant::now(); + if require_hashes { + warn_user!("Hash-checking mode (via `--require-hashes`) is not yet supported."); + } + let client_builder = BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls) @@ -289,6 +294,7 @@ pub(crate) async fn pip_sync( // Resolve with `--no-deps`. let options = OptionsBuilder::new() .dependency_mode(DependencyMode::Direct) + .require_hashes(require_hashes) .build(); // Create a bound on the progress bar, since we know the number of packages upfront. diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index 0e162184f..d6b525fad 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -593,6 +593,20 @@ struct PipSyncArgs { #[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] index_strategy: IndexStrategy, + /// Require a matching hash for each requirement. + /// + /// Hash-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. + /// + /// Hash-checking mode introduces a number of additional constraints: + /// - Git dependencies are not supported. + /// - Editable installs 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. + #[clap(long, hide = true)] + require_hashes: bool, + /// Attempt to use `keyring` for authentication for index urls /// /// Function's similar to `pip`'s `--keyring-provider subprocess` argument, @@ -867,6 +881,20 @@ struct PipInstallArgs { #[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")] index_strategy: IndexStrategy, + /// Require a matching hash for each requirement. + /// + /// Hash-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. + /// + /// Hash-checking mode introduces a number of additional constraints: + /// - Git dependencies are not supported. + /// - Editable installs 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. + #[clap(long, hide = true)] + require_hashes: bool, + /// Attempt to use `keyring` for authentication for index urls /// /// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently @@ -1650,6 +1678,7 @@ async fn run() -> Result { &reinstall, args.link_mode, args.compile, + args.require_hashes, index_urls, args.index_strategy, args.keyring_provider, @@ -1750,6 +1779,7 @@ async fn run() -> Result { reinstall, args.link_mode, args.compile, + args.require_hashes, setup_py, if args.offline { Connectivity::Offline From 48ba7df98a1631c9abf36fb7876f0e891440a834 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 14:38:42 -0400 Subject: [PATCH 088/110] Move `FlatIndex` into the `uv-resolver` crate (#2972) ## Summary This lets us remove circular dependencies (in the future, e.g., #2945) that arise from `FlatIndex` needing a bunch of resolver-specific abstractions (like incompatibilities, required hashes, etc.) that aren't necessary to _fetch_ the flat index entries. --- Cargo.lock | 1 - crates/uv-client/Cargo.toml | 1 - crates/uv-client/src/flat_index.rs | 192 +------------------- crates/uv-client/src/lib.rs | 2 +- crates/uv-dev/src/build.rs | 4 +- crates/uv-dev/src/resolve_cli.rs | 4 +- crates/uv-dev/src/resolve_many.rs | 4 +- crates/uv-dispatch/src/lib.rs | 4 +- crates/uv-resolver/src/flat_index.rs | 185 +++++++++++++++++++ crates/uv-resolver/src/lib.rs | 2 + crates/uv-resolver/src/resolver/mod.rs | 3 +- crates/uv-resolver/src/resolver/provider.rs | 3 +- crates/uv-resolver/src/version_map.rs | 3 +- crates/uv-resolver/tests/resolver.rs | 6 +- crates/uv/src/commands/pip_compile.rs | 8 +- crates/uv/src/commands/pip_install.rs | 7 +- crates/uv/src/commands/pip_sync.rs | 5 +- crates/uv/src/commands/venv.rs | 4 +- 18 files changed, 222 insertions(+), 216 deletions(-) create mode 100644 crates/uv-resolver/src/flat_index.rs diff --git a/Cargo.lock b/Cargo.lock index e87f84ce7..1dbf4a79b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4399,7 +4399,6 @@ dependencies = [ "reqwest-retry", "rkyv", "rmp-serde", - "rustc-hash", "serde", "serde_json", "sys-info", diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 10d0cee28..196d8a62e 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -34,7 +34,6 @@ reqwest-middleware = { workspace = true } reqwest-retry = { workspace = true } rkyv = { workspace = true } rmp-serde = { workspace = true } -rustc-hash = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sys-info = { workspace = true } diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index 5577af957..facd5dbc0 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -1,26 +1,17 @@ -use std::collections::btree_map::Entry; -use std::collections::BTreeMap; use std::path::PathBuf; use futures::{FutureExt, StreamExt}; use reqwest::Response; -use rustc_hash::FxHashMap; -use tracing::{debug, info_span, instrument, warn, Instrument}; + +use tracing::{debug, info_span, warn, Instrument}; use url::Url; -use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; -use distribution_types::{ - BuiltDist, Dist, File, FileLocation, FlatIndexLocation, IncompatibleSource, IncompatibleWheel, - IndexUrl, PrioritizedDist, RegistryBuiltDist, RegistrySourceDist, SourceDist, - SourceDistCompatibility, WheelCompatibility, -}; -use pep440_rs::Version; +use distribution_filename::DistFilename; +use distribution_types::{File, FileLocation, FlatIndexLocation, IndexUrl}; + use pep508_rs::VerbatimUrl; -use platform_tags::Tags; use uv_cache::{Cache, CacheBucket}; -use uv_configuration::{NoBinary, NoBuild}; -use uv_normalize::PackageName; use crate::cached_client::{CacheControl, CachedClientError}; use crate::html::SimpleHtml; @@ -38,10 +29,10 @@ pub enum FlatIndexError { #[derive(Debug, Default, Clone)] pub struct FlatIndexEntries { /// The list of `--find-links` entries. - entries: Vec<(DistFilename, File, IndexUrl)>, + pub entries: Vec<(DistFilename, File, IndexUrl)>, /// Whether any `--find-links` entries could not be resolved due to a lack of network /// connectivity. - offline: bool, + pub offline: bool, } impl FlatIndexEntries { @@ -258,172 +249,3 @@ impl<'a> FlatIndexClient<'a> { Ok(FlatIndexEntries::from_entries(dists)) } } - -/// A set of [`PrioritizedDist`] from a `--find-links` entry, indexed by [`PackageName`] -/// and [`Version`]. -#[derive(Debug, Clone, Default)] -pub struct FlatIndex { - /// The list of [`FlatDistributions`] from the `--find-links` entries, indexed by package name. - index: FxHashMap, - /// Whether any `--find-links` entries could not be resolved due to a lack of network - /// connectivity. - offline: bool, -} - -impl FlatIndex { - /// Collect all files from a `--find-links` target into a [`FlatIndex`]. - #[instrument(skip_all)] - pub fn from_entries( - entries: FlatIndexEntries, - tags: &Tags, - no_build: &NoBuild, - no_binary: &NoBinary, - ) -> Self { - // Collect compatible distributions. - let mut index = FxHashMap::default(); - for (filename, file, url) in entries.entries { - let distributions = index.entry(filename.name().clone()).or_default(); - Self::add_file( - distributions, - file, - filename, - tags, - no_build, - no_binary, - url, - ); - } - - // Collect offline entries. - let offline = entries.offline; - - Self { index, offline } - } - - fn add_file( - distributions: &mut FlatDistributions, - file: File, - filename: DistFilename, - tags: &Tags, - no_build: &NoBuild, - no_binary: &NoBinary, - index: IndexUrl, - ) { - // No `requires-python` here: for source distributions, we don't have that information; - // for wheels, we read it lazily only when selected. - match filename { - DistFilename::WheelFilename(filename) => { - let version = filename.version.clone(); - - let compatibility = Self::wheel_compatibility(&filename, tags, no_binary); - let dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist { - filename, - file: Box::new(file), - index, - })); - match distributions.0.entry(version) { - Entry::Occupied(mut entry) => { - entry.get_mut().insert_built(dist, vec![], compatibility); - } - Entry::Vacant(entry) => { - entry.insert(PrioritizedDist::from_built(dist, vec![], compatibility)); - } - } - } - DistFilename::SourceDistFilename(filename) => { - let compatibility = Self::source_dist_compatibility(&filename, no_build); - let dist = Dist::Source(SourceDist::Registry(RegistrySourceDist { - filename: filename.clone(), - file: Box::new(file), - index, - })); - match distributions.0.entry(filename.version) { - Entry::Occupied(mut entry) => { - entry.get_mut().insert_source(dist, vec![], compatibility); - } - Entry::Vacant(entry) => { - entry.insert(PrioritizedDist::from_source(dist, vec![], compatibility)); - } - } - } - } - } - - fn source_dist_compatibility( - filename: &SourceDistFilename, - no_build: &NoBuild, - ) -> SourceDistCompatibility { - // Check if source distributions are allowed for this package. - let no_build = match no_build { - NoBuild::None => false, - NoBuild::All => true, - NoBuild::Packages(packages) => packages.contains(&filename.name), - }; - - if no_build { - return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild); - } - - SourceDistCompatibility::Compatible - } - - fn wheel_compatibility( - filename: &WheelFilename, - tags: &Tags, - no_binary: &NoBinary, - ) -> WheelCompatibility { - // Check if binaries are allowed for this package. - let no_binary = match no_binary { - NoBinary::None => false, - NoBinary::All => true, - NoBinary::Packages(packages) => packages.contains(&filename.name), - }; - - if no_binary { - return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary); - } - - // Determine a compatibility for the wheel based on tags. - WheelCompatibility::from(filename.compatibility(tags)) - } - - /// Get the [`FlatDistributions`] for the given package name. - pub fn get(&self, package_name: &PackageName) -> Option<&FlatDistributions> { - self.index.get(package_name) - } - - /// Returns `true` if there are any offline `--find-links` entries. - pub fn offline(&self) -> bool { - self.offline - } -} - -/// A set of [`PrioritizedDist`] from a `--find-links` entry for a single package, indexed -/// by [`Version`]. -#[derive(Debug, Clone, Default)] -pub struct FlatDistributions(BTreeMap); - -impl FlatDistributions { - pub fn iter(&self) -> impl Iterator { - self.0.iter() - } - - pub fn remove(&mut self, version: &Version) -> Option { - self.0.remove(version) - } -} - -impl IntoIterator for FlatDistributions { - type Item = (Version, PrioritizedDist); - type IntoIter = std::collections::btree_map::IntoIter; - - fn into_iter(self) -> Self::IntoIter { - self.0.into_iter() - } -} - -impl From for BTreeMap { - fn from(distributions: FlatDistributions) -> Self { - distributions.0 - } -} diff --git a/crates/uv-client/src/lib.rs b/crates/uv-client/src/lib.rs index 1e2adc731..24f842d8e 100644 --- a/crates/uv-client/src/lib.rs +++ b/crates/uv-client/src/lib.rs @@ -1,7 +1,7 @@ pub use base_client::{BaseClient, BaseClientBuilder}; pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy}; pub use error::{BetterReqwestError, Error, ErrorKind}; -pub use flat_index::{FlatDistributions, FlatIndex, FlatIndexClient, FlatIndexError}; +pub use flat_index::{FlatIndexClient, FlatIndexEntries, FlatIndexError}; pub use linehaul::LineHaul; pub use registry_client::{ Connectivity, RegistryClient, RegistryClientBuilder, SimpleMetadata, SimpleMetadatum, diff --git a/crates/uv-dev/src/build.rs b/crates/uv-dev/src/build.rs index 60457a170..ae6095086 100644 --- a/crates/uv-dev/src/build.rs +++ b/crates/uv-dev/src/build.rs @@ -9,11 +9,11 @@ use distribution_types::IndexLocations; use rustc_hash::FxHashMap; use uv_build::{SourceBuild, SourceBuildContext}; use uv_cache::{Cache, CacheArgs}; -use uv_client::{FlatIndex, RegistryClientBuilder}; +use uv_client::RegistryClientBuilder; use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_interpreter::PythonEnvironment; -use uv_resolver::InMemoryIndex; +use uv_resolver::{FlatIndex, InMemoryIndex}; use uv_types::{BuildContext, BuildIsolation, InFlight}; #[derive(Parser)] diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index 27aa0748b..969ee18c6 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -12,12 +12,12 @@ use petgraph::dot::{Config as DotConfig, Dot}; use distribution_types::{FlatIndexLocation, IndexLocations, IndexUrl, Resolution}; use pep508_rs::Requirement; use uv_cache::{Cache, CacheArgs}; -use uv_client::{FlatIndex, FlatIndexClient, RegistryClientBuilder}; +use uv_client::{FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ConfigSettings, NoBinary, NoBuild, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_installer::SitePackages; use uv_interpreter::PythonEnvironment; -use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; +use uv_resolver::{FlatIndex, InMemoryIndex, Manifest, Options, Resolver}; use uv_types::{BuildIsolation, InFlight}; #[derive(ValueEnum, Default, Clone)] diff --git a/crates/uv-dev/src/resolve_many.rs b/crates/uv-dev/src/resolve_many.rs index 07f1ddc18..761fcbed6 100644 --- a/crates/uv-dev/src/resolve_many.rs +++ b/crates/uv-dev/src/resolve_many.rs @@ -14,12 +14,12 @@ use distribution_types::IndexLocations; use pep440_rs::{Version, VersionSpecifier, VersionSpecifiers}; use pep508_rs::{Requirement, VersionOrUrl}; use uv_cache::{Cache, CacheArgs}; -use uv_client::{FlatIndex, OwnedArchive, RegistryClient, RegistryClientBuilder}; +use uv_client::{OwnedArchive, RegistryClient, RegistryClientBuilder}; use uv_configuration::{ConfigSettings, NoBinary, NoBuild, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; -use uv_resolver::InMemoryIndex; +use uv_resolver::{FlatIndex, InMemoryIndex}; use uv_types::{BuildContext, BuildIsolation, InFlight}; #[derive(Parser)] diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 072db577a..b35961d54 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -16,11 +16,11 @@ use distribution_types::{IndexLocations, Name, Resolution, SourceDist}; use pep508_rs::Requirement; use uv_build::{SourceBuild, SourceBuildContext}; use uv_cache::Cache; -use uv_client::{FlatIndex, RegistryClient}; +use uv_client::RegistryClient; use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, Reinstall, SetupPyStrategy}; use uv_installer::{Downloader, Installer, Plan, Planner, SitePackages}; use uv_interpreter::{Interpreter, PythonEnvironment}; -use uv_resolver::{InMemoryIndex, Manifest, Options, Resolver}; +use uv_resolver::{FlatIndex, InMemoryIndex, Manifest, Options, Resolver}; use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, InFlight}; /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] diff --git a/crates/uv-resolver/src/flat_index.rs b/crates/uv-resolver/src/flat_index.rs new file mode 100644 index 000000000..820ad310a --- /dev/null +++ b/crates/uv-resolver/src/flat_index.rs @@ -0,0 +1,185 @@ +use std::collections::btree_map::Entry; +use std::collections::BTreeMap; + +use rustc_hash::FxHashMap; +use tracing::instrument; + +use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; +use distribution_types::{ + BuiltDist, Dist, File, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist, + RegistryBuiltDist, RegistrySourceDist, SourceDist, SourceDistCompatibility, WheelCompatibility, +}; +use pep440_rs::Version; +use platform_tags::Tags; +use uv_client::FlatIndexEntries; +use uv_configuration::{NoBinary, NoBuild}; +use uv_normalize::PackageName; + +/// A set of [`PrioritizedDist`] from a `--find-links` entry, indexed by [`PackageName`] +/// and [`Version`]. +#[derive(Debug, Clone, Default)] +pub struct FlatIndex { + /// The list of [`FlatDistributions`] from the `--find-links` entries, indexed by package name. + index: FxHashMap, + /// Whether any `--find-links` entries could not be resolved due to a lack of network + /// connectivity. + offline: bool, +} + +impl FlatIndex { + /// Collect all files from a `--find-links` target into a [`FlatIndex`]. + #[instrument(skip_all)] + pub fn from_entries( + entries: FlatIndexEntries, + tags: &Tags, + no_build: &NoBuild, + no_binary: &NoBinary, + ) -> Self { + // Collect compatible distributions. + let mut index = FxHashMap::default(); + for (filename, file, url) in entries.entries { + let distributions = index.entry(filename.name().clone()).or_default(); + Self::add_file( + distributions, + file, + filename, + tags, + no_build, + no_binary, + url, + ); + } + + // Collect offline entries. + let offline = entries.offline; + + Self { index, offline } + } + + fn add_file( + distributions: &mut FlatDistributions, + file: File, + filename: DistFilename, + tags: &Tags, + no_build: &NoBuild, + no_binary: &NoBinary, + index: IndexUrl, + ) { + // No `requires-python` here: for source distributions, we don't have that information; + // for wheels, we read it lazily only when selected. + match filename { + DistFilename::WheelFilename(filename) => { + let version = filename.version.clone(); + + let compatibility = Self::wheel_compatibility(&filename, tags, no_binary); + let dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist { + filename, + file: Box::new(file), + index, + })); + match distributions.0.entry(version) { + Entry::Occupied(mut entry) => { + entry.get_mut().insert_built(dist, vec![], compatibility); + } + Entry::Vacant(entry) => { + entry.insert(PrioritizedDist::from_built(dist, vec![], compatibility)); + } + } + } + DistFilename::SourceDistFilename(filename) => { + let compatibility = Self::source_dist_compatibility(&filename, no_build); + let dist = Dist::Source(SourceDist::Registry(RegistrySourceDist { + filename: filename.clone(), + file: Box::new(file), + index, + })); + match distributions.0.entry(filename.version) { + Entry::Occupied(mut entry) => { + entry.get_mut().insert_source(dist, vec![], compatibility); + } + Entry::Vacant(entry) => { + entry.insert(PrioritizedDist::from_source(dist, vec![], compatibility)); + } + } + } + } + } + + fn source_dist_compatibility( + filename: &SourceDistFilename, + no_build: &NoBuild, + ) -> SourceDistCompatibility { + // Check if source distributions are allowed for this package. + let no_build = match no_build { + NoBuild::None => false, + NoBuild::All => true, + NoBuild::Packages(packages) => packages.contains(&filename.name), + }; + + if no_build { + return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild); + } + + SourceDistCompatibility::Compatible + } + + fn wheel_compatibility( + filename: &WheelFilename, + tags: &Tags, + no_binary: &NoBinary, + ) -> WheelCompatibility { + // Check if binaries are allowed for this package. + let no_binary = match no_binary { + NoBinary::None => false, + NoBinary::All => true, + NoBinary::Packages(packages) => packages.contains(&filename.name), + }; + + if no_binary { + return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary); + } + + // Determine a compatibility for the wheel based on tags. + WheelCompatibility::from(filename.compatibility(tags)) + } + + /// Get the [`FlatDistributions`] for the given package name. + pub fn get(&self, package_name: &PackageName) -> Option<&FlatDistributions> { + self.index.get(package_name) + } + + /// Returns `true` if there are any offline `--find-links` entries. + pub fn offline(&self) -> bool { + self.offline + } +} + +/// A set of [`PrioritizedDist`] from a `--find-links` entry for a single package, indexed +/// by [`Version`]. +#[derive(Debug, Clone, Default)] +pub struct FlatDistributions(BTreeMap); + +impl FlatDistributions { + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } + + pub fn remove(&mut self, version: &Version) -> Option { + self.0.remove(version) + } +} + +impl IntoIterator for FlatDistributions { + type Item = (Version, PrioritizedDist); + type IntoIter = std::collections::btree_map::IntoIter; + + fn into_iter(self) -> Self::IntoIter { + self.0.into_iter() + } +} + +impl From for BTreeMap { + fn from(distributions: FlatDistributions) -> Self { + distributions.0 + } +} diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 9c083ee3f..c7fd1aea1 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -1,6 +1,7 @@ pub use dependency_mode::DependencyMode; pub use error::ResolveError; pub use exclusions::Exclusions; +pub use flat_index::FlatIndex; pub use manifest::Manifest; pub use options::{Options, OptionsBuilder}; pub use preferences::{Preference, PreferenceError}; @@ -24,6 +25,7 @@ mod dependency_provider; mod editables; mod error; mod exclusions; +mod flat_index; mod manifest; mod options; mod pins; diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 5e3723f4c..1a4d85354 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -26,7 +26,7 @@ use pep508_rs::{MarkerEnvironment, Requirement}; use platform_tags::Tags; use pypi_types::Metadata23; pub(crate) use urls::Urls; -use uv_client::{FlatIndex, RegistryClient}; +use uv_client::RegistryClient; use uv_configuration::{Constraints, Overrides}; use uv_distribution::DistributionDatabase; use uv_interpreter::Interpreter; @@ -36,6 +36,7 @@ use uv_types::{BuildContext, InstalledPackagesProvider}; use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::editables::Editables; use crate::error::ResolveError; +use crate::flat_index::FlatIndex; use crate::manifest::Manifest; use crate::pins::FilePins; use crate::preferences::Preferences; diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index c778e780e..77078fd50 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -6,12 +6,13 @@ use chrono::{DateTime, Utc}; use distribution_types::{Dist, IndexLocations}; use platform_tags::Tags; use pypi_types::Metadata23; -use uv_client::{FlatIndex, RegistryClient}; +use uv_client::RegistryClient; use uv_configuration::{NoBinary, NoBuild}; use uv_distribution::DistributionDatabase; use uv_normalize::PackageName; use uv_types::BuildContext; +use crate::flat_index::FlatIndex; use crate::python_requirement::PythonRequirement; use crate::version_map::VersionMap; use crate::yanks::AllowedYanks; diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 8934f98f0..f2608425e 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -14,11 +14,12 @@ use pep440_rs::{Version, VersionSpecifiers}; use platform_tags::Tags; use pypi_types::{HashDigest, Yanked}; use rkyv::{de::deserializers::SharedDeserializeMap, Deserialize}; -use uv_client::{FlatDistributions, OwnedArchive, SimpleMetadata, VersionFiles}; +use uv_client::{OwnedArchive, SimpleMetadata, VersionFiles}; use uv_configuration::{NoBinary, NoBuild}; use uv_normalize::PackageName; use uv_warnings::warn_user_once; +use crate::flat_index::FlatDistributions; use crate::{python_requirement::PythonRequirement, yanks::AllowedYanks}; /// A map from versions to distributions. diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 69734adbb..16cb9e195 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -14,12 +14,12 @@ use distribution_types::{IndexLocations, Resolution, SourceDist}; use pep508_rs::{MarkerEnvironment, Requirement, StringVersion}; use platform_tags::{Arch, Os, Platform, Tags}; use uv_cache::Cache; -use uv_client::{FlatIndex, RegistryClientBuilder}; +use uv_client::RegistryClientBuilder; use uv_configuration::{BuildKind, Constraints, NoBinary, NoBuild, Overrides, SetupPyStrategy}; use uv_interpreter::{find_default_python, Interpreter, PythonEnvironment}; use uv_resolver::{ - DisplayResolutionGraph, Exclusions, InMemoryIndex, Manifest, Options, OptionsBuilder, - PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, + DisplayResolutionGraph, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, + OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, SourceBuildTrait}; diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 032586d94..4b2235ec7 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -19,9 +19,7 @@ use platform_tags::Tags; use requirements_txt::EditableRequirement; use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE}; use uv_cache::Cache; -use uv_client::{ - BaseClientBuilder, Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder, -}; +use uv_client::{BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ ConfigSettings, Constraints, IndexStrategy, NoBinary, NoBuild, Overrides, SetupPyStrategy, Upgrade, @@ -36,8 +34,8 @@ use uv_requirements::{ RequirementsSource, RequirementsSpecification, SourceTreeResolver, }; use uv_resolver::{ - AnnotationStyle, DependencyMode, DisplayResolutionGraph, Exclusions, InMemoryIndex, Manifest, - OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, + AnnotationStyle, DependencyMode, DisplayResolutionGraph, Exclusions, FlatIndex, InMemoryIndex, + Manifest, OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, }; use uv_toolchain::PythonVersion; use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight}; diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 280c0c0b7..2135634a5 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -21,8 +21,7 @@ use requirements_txt::EditableRequirement; use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE}; use uv_cache::Cache; use uv_client::{ - BaseClientBuilder, Connectivity, FlatIndex, FlatIndexClient, RegistryClient, - RegistryClientBuilder, + BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClient, RegistryClientBuilder, }; use uv_configuration::{ ConfigSettings, Constraints, IndexStrategy, NoBinary, NoBuild, Overrides, Reinstall, @@ -38,8 +37,8 @@ use uv_requirements::{ RequirementsSpecification, SourceTreeResolver, }; use uv_resolver::{ - DependencyMode, Exclusions, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, - Preference, ResolutionGraph, ResolutionMode, Resolver, + DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, OptionsBuilder, + PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; use uv_types::{BuildIsolation, InFlight}; use uv_warnings::warn_user; diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index b5d991ad1..70a9f4998 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -16,8 +16,7 @@ use requirements_txt::EditableRequirement; use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE}; use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache}; use uv_client::{ - BaseClientBuilder, Connectivity, FlatIndex, FlatIndexClient, RegistryClient, - RegistryClientBuilder, + BaseClientBuilder, Connectivity, FlatIndexClient, RegistryClient, RegistryClientBuilder, }; use uv_configuration::{ ConfigSettings, IndexStrategy, NoBinary, NoBuild, Reinstall, SetupPyStrategy, @@ -30,7 +29,7 @@ use uv_requirements::{ ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification, SourceTreeResolver, }; -use uv_resolver::{DependencyMode, InMemoryIndex, Manifest, OptionsBuilder, Resolver}; +use uv_resolver::{DependencyMode, FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, Resolver}; use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight}; use uv_warnings::warn_user; diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 153a5f756..8c1f943fd 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -15,12 +15,12 @@ use distribution_types::{DistributionMetadata, IndexLocations, Name, ResolvedDis use pep508_rs::Requirement; use uv_auth::{KeyringProvider, GLOBAL_AUTH_STORE}; use uv_cache::Cache; -use uv_client::{Connectivity, FlatIndex, FlatIndexClient, RegistryClientBuilder}; +use uv_client::{Connectivity, FlatIndexClient, RegistryClientBuilder}; use uv_configuration::{ConfigSettings, IndexStrategy, NoBinary, NoBuild, SetupPyStrategy}; use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_interpreter::{find_default_python, find_requested_python, Error}; -use uv_resolver::{InMemoryIndex, OptionsBuilder}; +use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder}; use uv_types::{BuildContext, BuildIsolation, InFlight}; use crate::commands::ExitStatus; From ddf02e7d5fba8229016d5c1ac29978eb5490f2a7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 14:56:39 -0400 Subject: [PATCH 089/110] Remove unused `task-local-extensions` dependency (#2974) ## Summary Made obsolete with the `reqwest` upgrade. --- Cargo.lock | 28 ---------------------------- Cargo.toml | 1 - crates/uv-client/Cargo.toml | 2 -- crates/uv-configuration/Cargo.toml | 2 -- crates/uv-dev/Cargo.toml | 5 ----- 5 files changed, 38 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1dbf4a79b..fcbc0755b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2289,16 +2289,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" -[[package]] -name = "os_info" -version = "3.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e" -dependencies = [ - "log", - "winapi", -] - [[package]] name = "overload" version = "0.1.1" @@ -3598,15 +3588,6 @@ version = "0.12.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" -[[package]] -name = "task-local-extensions" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba323866e5d033818e3240feeb9f7db2c4296674e4d9e16b97b7bf8f490434e8" -dependencies = [ - "pin-utils", -] - [[package]] name = "temp-dir" version = "0.1.13" @@ -4389,7 +4370,6 @@ dependencies = [ "hyper-util", "insta", "install-wheel-rs", - "os_info", "pep440_rs", "pep508_rs", "platform-tags", @@ -4402,7 +4382,6 @@ dependencies = [ "serde", "serde_json", "sys-info", - "task-local-extensions", "tempfile", "thiserror", "tl", @@ -4426,13 +4405,11 @@ version = "0.0.1" dependencies = [ "anyhow", "clap", - "distribution-types", "itertools 0.12.1", "pep508_rs", "rustc-hash", "serde", "serde_json", - "uv-cache", "uv-normalize", ] @@ -4456,27 +4433,22 @@ dependencies = [ "pep508_rs", "petgraph", "poloto", - "reqwest", "resvg", "rustc-hash", "serde", "serde_json", "tagu", - "tempfile", "tikv-jemallocator", "tokio", - "tokio-util", "tracing", "tracing-durations-export", "tracing-indicatif", "tracing-subscriber", - "url", "uv-build", "uv-cache", "uv-client", "uv-configuration", "uv-dispatch", - "uv-extract", "uv-fs", "uv-installer", "uv-interpreter", diff --git a/Cargo.toml b/Cargo.toml index 8da70b7ab..ae1963ad2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,7 +122,6 @@ serde_json = { version = "1.0.114" } sha1 = { version = "0.10.6" } sha2 = { version = "0.10.8" } sys-info = { version = "0.9.1" } -task-local-extensions = { version = "0.1.4" } tempfile = { version = "3.9.0" } textwrap = { version = "0.16.1" } thiserror = { version = "1.0.56" } diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 196d8a62e..f7db9eebb 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -37,7 +37,6 @@ rmp-serde = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sys-info = { workspace = true } -task-local-extensions = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tl = { workspace = true } @@ -53,5 +52,4 @@ http-body-util = { version = "0.1.0" } hyper = { version = "1.2.0", features = ["server", "http1"] } hyper-util = { version = "0.1.3", features = ["tokio"] } insta = { version = "1.36.1" } -os_info = { version = "=3.7.0", default-features = false } tokio = { workspace = true, features = ["fs", "macros"] } diff --git a/crates/uv-configuration/Cargo.toml b/crates/uv-configuration/Cargo.toml index 67524ef8d..8a176161e 100644 --- a/crates/uv-configuration/Cargo.toml +++ b/crates/uv-configuration/Cargo.toml @@ -13,9 +13,7 @@ license = { workspace = true } workspace = true [dependencies] -distribution-types = { workspace = true } pep508_rs = { workspace = true } -uv-cache = { workspace = true } uv-normalize = { workspace = true } anyhow = { workspace = true } diff --git a/crates/uv-dev/Cargo.toml b/crates/uv-dev/Cargo.toml index ab56cb0ec..7f9d323c7 100644 --- a/crates/uv-dev/Cargo.toml +++ b/crates/uv-dev/Cargo.toml @@ -25,7 +25,6 @@ uv-cache = { workspace = true, features = ["clap"] } uv-client = { workspace = true } uv-configuration = { workspace = true } uv-dispatch = { workspace = true } -uv-extract = { workspace = true } uv-fs = { workspace = true } uv-installer = { workspace = true } uv-interpreter = { workspace = true } @@ -49,18 +48,14 @@ petgraph = { workspace = true } poloto = { version = "19.1.2" } resvg = { version = "0.29.0" } rustc-hash = { workspace = true } -reqwest = { workspace = true } -tempfile = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tagu = { version = "0.1.6" } tokio = { workspace = true } -tokio-util = { workspace = true, features = ["compat"] } tracing = { workspace = true } tracing-durations-export = { workspace = true, features = ["plot"] } tracing-indicatif = { workspace = true } tracing-subscriber = { workspace = true } -url = { workspace = true } walkdir = { workspace = true } [target.'cfg(target_os = "windows")'.dependencies] From 715a309dd57f83c34bc4ed5c5cc1162afcec9f55 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 14:58:34 -0400 Subject: [PATCH 090/110] Remove unused `--output-file` from `pip install` (#2975) ## Summary This doesn't do anything. I suspect it was a copy-paste error. --- crates/uv/src/main.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index d6b525fad..f20674194 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -831,10 +831,6 @@ struct PipInstallArgs { #[clap(long, hide = true, conflicts_with = "prerelease")] pre: bool, - /// Write the compiled requirements to the given `requirements.txt` file. - #[clap(long, short)] - output_file: Option, - /// The URL of the Python package index (by default: ). /// /// The index given by this flag is given lower priority than all other From 1f3b5bb0936f5e732df3e5c05ff5dd5438b0af05 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 15:09:03 -0400 Subject: [PATCH 091/110] Add hash-checking support to `install` and `sync` (#2945) ## Summary This PR adds support for hash-checking mode in `pip install` and `pip sync`. It's a large change, both in terms of the size of the diff and the modifications in behavior, but it's also one that's hard to merge in pieces (at least, with any test coverage) since it needs to work end-to-end to be useful and testable. Here are some of the most important highlights: - We store hashes in the cache. Where we previously stored pointers to unzipped wheels in the `archives` directory, we now store pointers with a set of known hashes. So every pointer to an unzipped wheel also includes its known hashes. - By default, we don't compute any hashes. If the user runs with `--require-hashes`, and the cache doesn't contain those hashes, we invalidate the cache, redownload the wheel, and compute the hashes as we go. For users that don't run with `--require-hashes`, there will be no change in performance. For users that _do_, the only change will be if they don't run with `--generate-hashes` -- then they may see some repeated work between resolution and installation, if they use `pip compile` then `pip sync`. - Many of the distribution types now include a `hashes` field, like `CachedDist` and `LocalWheel`. - Our behavior is similar to pip, in that we enforce hashes when pulling any remote distributions, and when pulling from our own cache. Like pip, though, we _don't_ enforce hashes if a distribution is _already_ installed. - Hash validity is enforced in a few different places: 1. During resolution, we enforce hash validity based on the hashes reported by the registry. If we need to access a source distribution, though, we then enforce hash validity at that point too, prior to running any untrusted code. (This is enforced in the distribution database.) 2. In the install plan, we _only_ add cached distributions that have matching hashes. If a cached distribution is missing any hashes, or the hashes don't match, we don't return them from the install plan. 3. In the downloader, we _only_ return distributions with matching hashes. 4. The final combination of "things we install" are: (1) the wheels from the cache, and (2) the downloaded wheels. So this ensures that we never install any mismatching distributions. - Like pip, if `--require-hashes` is provided, we require that _all_ distributions are pinned with either `==` or a direct URL. We also require that _all_ distributions have hashes. There are a few notable TODOs: - We don't support hash-checking mode for unnamed requirements. These should be _somewhat_ rare, though? Since `pip compile` never outputs unnamed requirements. I can fix this, it's just some additional work. - We don't automatically enable `--require-hashes` with a hash exists in the requirements file. We require `--require-hashes`. Closes #474. ## Test Plan I'd like to add some tests for registries that report incorrect hashes, but otherwise: `cargo test` --- Cargo.lock | 19 + Cargo.toml | 1 + PIP_COMPATIBILITY.md | 8 - crates/distribution-types/src/cached.rs | 48 +- crates/distribution-types/src/hashed.rs | 27 + crates/distribution-types/src/lib.rs | 2 + .../src/prioritized_distribution.rs | 39 +- crates/pep508-rs/src/lib.rs | 2 +- crates/uv-cache/src/lib.rs | 4 +- crates/uv-client/Cargo.toml | 4 +- crates/uv-client/src/cached_client.rs | 28 + crates/uv-client/src/flat_index.rs | 3 - crates/uv-dev/src/resolve_cli.rs | 5 +- crates/uv-dispatch/src/lib.rs | 10 +- crates/uv-distribution/Cargo.toml | 2 + crates/uv-distribution/src/archive.rs | 36 + .../src/distribution_database.rs | 278 +++- crates/uv-distribution/src/download.rs | 14 +- crates/uv-distribution/src/error.rs | 45 + .../src/index/built_wheel_index.rs | 41 +- .../uv-distribution/src/index/cached_wheel.rs | 70 +- .../src/index/registry_wheel_index.rs | 90 +- crates/uv-distribution/src/lib.rs | 4 +- .../src/source/built_wheel_metadata.rs | 20 +- crates/uv-distribution/src/source/mod.rs | 289 +++- crates/uv-distribution/src/source/revision.rs | 40 +- crates/uv-extract/Cargo.toml | 4 + crates/uv-extract/src/hash.rs | 146 ++ crates/uv-extract/src/lib.rs | 1 + crates/uv-extract/src/stream.rs | 3 +- crates/uv-installer/Cargo.toml | 3 +- crates/uv-installer/src/downloader.rs | 28 +- crates/uv-installer/src/plan.rs | 98 +- crates/uv-requirements/src/lookahead.rs | 13 +- crates/uv-requirements/src/source_tree.rs | 17 +- crates/uv-requirements/src/specification.rs | 15 +- crates/uv-requirements/src/unnamed.rs | 18 +- crates/uv-resolver/src/error.rs | 3 + crates/uv-resolver/src/flat_index.rs | 51 +- crates/uv-resolver/src/hash_checking_mode.rs | 15 + crates/uv-resolver/src/lib.rs | 2 + crates/uv-resolver/src/options.rs | 21 +- crates/uv-resolver/src/resolver/mod.rs | 29 +- crates/uv-resolver/src/resolver/provider.rs | 11 +- crates/uv-resolver/src/version_map.rs | 45 +- crates/uv-resolver/tests/resolver.rs | 6 +- crates/uv-types/Cargo.toml | 3 + crates/uv-types/src/hashes.rs | 99 ++ crates/uv-types/src/lib.rs | 2 + crates/uv/src/commands/pip_compile.rs | 16 +- crates/uv/src/commands/pip_install.rs | 82 +- crates/uv/src/commands/pip_sync.rs | 60 +- crates/uv/src/commands/venv.rs | 10 +- crates/uv/tests/cache_prune.rs | 2 +- crates/uv/tests/pip_install.rs | 201 +++ crates/uv/tests/pip_sync.rs | 1386 +++++++++++++++++ 56 files changed, 3186 insertions(+), 333 deletions(-) create mode 100644 crates/distribution-types/src/hashed.rs create mode 100644 crates/uv-distribution/src/archive.rs create mode 100644 crates/uv-extract/src/hash.rs create mode 100644 crates/uv-resolver/src/hash_checking_mode.rs create mode 100644 crates/uv-types/src/hashes.rs diff --git a/Cargo.lock b/Cargo.lock index fcbc0755b..a70416778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2024,6 +2024,16 @@ dependencies = [ "regex-automata 0.1.10", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest", +] + [[package]] name = "memchr" version = "2.7.2" @@ -4491,6 +4501,7 @@ dependencies = [ "fs-err", "futures", "install-wheel-rs", + "md-5", "nanoid", "once_cell", "pep440_rs", @@ -4502,6 +4513,7 @@ dependencies = [ "rmp-serde", "rustc-hash", "serde", + "sha2", "tempfile", "thiserror", "tokio", @@ -4527,8 +4539,11 @@ dependencies = [ "async_zip", "fs-err", "futures", + "md-5", + "pypi-types", "rayon", "rustc-hash", + "sha2", "thiserror", "tokio", "tokio-tar", @@ -4593,6 +4608,7 @@ dependencies = [ "pypi-types", "rayon", "requirements-txt", + "rmp-serde", "rustc-hash", "serde", "tempfile", @@ -4766,10 +4782,13 @@ dependencies = [ "distribution-types", "itertools 0.12.1", "once-map", + "pep440_rs", "pep508_rs", + "pypi-types", "rustc-hash", "serde", "serde_json", + "thiserror", "uv-cache", "uv-configuration", "uv-interpreter", diff --git a/Cargo.toml b/Cargo.toml index ae1963ad2..b253d8a36 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -94,6 +94,7 @@ indoc = { version = "2.0.4" } itertools = { version = "0.12.1" } junction = { version = "1.0.0" } mailparse = { version = "0.14.0" } +md-5 = { version = "0.10.6" } miette = { version = "7.2.0" } nanoid = { version = "0.4.0" } once_cell = { version = "1.19.0" } diff --git a/PIP_COMPATIBILITY.md b/PIP_COMPATIBILITY.md index 594c60096..df70d3487 100644 --- a/PIP_COMPATIBILITY.md +++ b/PIP_COMPATIBILITY.md @@ -259,14 +259,6 @@ When uv resolutions differ from `pip` in undesirable ways, it's often a sign tha are too loose, and that the user should consider tightening them. For example, in the case of `starlette` and `fastapi`, the user could require `fastapi>=0.110.0`. -## Hash-checking mode - -While uv will include hashes via `uv pip compile --generate-hashes`, it does not support -hash-checking mode, which is a feature of `pip` that allows users to verify the integrity of -downloaded packages by checking their hashes against those provided in the `requirements.txt` file. - -In the future, uv will support hash-checking mode. For more, see [#474](https://github.com/astral-sh/uv/issues/474). - ## `pip check` At present, `uv pip check` will surface the following diagnostics: diff --git a/crates/distribution-types/src/cached.rs b/crates/distribution-types/src/cached.rs index 2a0da761d..9661d3cfe 100644 --- a/crates/distribution-types/src/cached.rs +++ b/crates/distribution-types/src/cached.rs @@ -4,9 +4,11 @@ use anyhow::Result; use distribution_filename::WheelFilename; use pep508_rs::VerbatimUrl; +use pypi_types::HashDigest; use uv_normalize::PackageName; use crate::direct_url::{DirectUrl, LocalFileUrl}; +use crate::hashed::Hashed; use crate::{ BuiltDist, Dist, DistributionMetadata, InstalledMetadata, InstalledVersion, Name, SourceDist, VersionOrUrl, @@ -25,6 +27,7 @@ pub enum CachedDist { pub struct CachedRegistryDist { pub filename: WheelFilename, pub path: PathBuf, + pub hashes: Vec, } #[derive(Debug, Clone)] @@ -33,45 +36,60 @@ pub struct CachedDirectUrlDist { pub url: VerbatimUrl, pub path: PathBuf, pub editable: bool, + pub hashes: Vec, } impl CachedDist { /// Initialize a [`CachedDist`] from a [`Dist`]. - pub fn from_remote(remote: Dist, filename: WheelFilename, path: PathBuf) -> Self { + pub fn from_remote( + remote: Dist, + filename: WheelFilename, + hashes: Vec, + path: PathBuf, + ) -> Self { match remote { - Dist::Built(BuiltDist::Registry(_dist)) => { - Self::Registry(CachedRegistryDist { filename, path }) - } + Dist::Built(BuiltDist::Registry(_dist)) => Self::Registry(CachedRegistryDist { + filename, + path, + hashes, + }), Dist::Built(BuiltDist::DirectUrl(dist)) => Self::Url(CachedDirectUrlDist { filename, url: dist.url, + hashes, path, editable: false, }), Dist::Built(BuiltDist::Path(dist)) => Self::Url(CachedDirectUrlDist { filename, url: dist.url, + hashes, path, editable: false, }), - Dist::Source(SourceDist::Registry(_dist)) => { - Self::Registry(CachedRegistryDist { filename, path }) - } + Dist::Source(SourceDist::Registry(_dist)) => Self::Registry(CachedRegistryDist { + filename, + path, + hashes, + }), Dist::Source(SourceDist::DirectUrl(dist)) => Self::Url(CachedDirectUrlDist { filename, url: dist.url, + hashes, path, editable: false, }), Dist::Source(SourceDist::Git(dist)) => Self::Url(CachedDirectUrlDist { filename, url: dist.url, + hashes, path, editable: false, }), Dist::Source(SourceDist::Path(dist)) => Self::Url(CachedDirectUrlDist { filename, url: dist.url, + hashes, path, editable: dist.editable, }), @@ -104,6 +122,7 @@ impl CachedDist { } } + /// Returns `true` if the distribution is editable. pub fn editable(&self) -> bool { match self { Self::Registry(_) => false, @@ -111,6 +130,7 @@ impl CachedDist { } } + /// Returns the [`WheelFilename`] of the distribution. pub fn filename(&self) -> &WheelFilename { match self { Self::Registry(dist) => &dist.filename, @@ -119,12 +139,24 @@ impl CachedDist { } } +impl Hashed for CachedRegistryDist { + fn hashes(&self) -> &[HashDigest] { + &self.hashes + } +} + impl CachedDirectUrlDist { /// Initialize a [`CachedDirectUrlDist`] from a [`WheelFilename`], [`url::Url`], and [`Path`]. - pub fn from_url(filename: WheelFilename, url: VerbatimUrl, path: PathBuf) -> Self { + pub fn from_url( + filename: WheelFilename, + url: VerbatimUrl, + hashes: Vec, + path: PathBuf, + ) -> Self { Self { filename, url, + hashes, path, editable: false, } diff --git a/crates/distribution-types/src/hashed.rs b/crates/distribution-types/src/hashed.rs new file mode 100644 index 000000000..c8185021c --- /dev/null +++ b/crates/distribution-types/src/hashed.rs @@ -0,0 +1,27 @@ +use pypi_types::HashDigest; + +pub trait Hashed { + /// Return the [`HashDigest`]s for the archive. + fn hashes(&self) -> &[HashDigest]; + + /// Returns `true` if the archive satisfies the given hashes. + fn satisfies(&self, hashes: &[HashDigest]) -> bool { + if hashes.is_empty() { + true + } else { + self.hashes().iter().any(|hash| hashes.contains(hash)) + } + } + + /// Returns `true` if the archive includes a hash for at least one of the given algorithms. + fn has_digests(&self, hashes: &[HashDigest]) -> bool { + if hashes.is_empty() { + true + } else { + hashes + .iter() + .map(HashDigest::algorithm) + .any(|algorithm| self.hashes().iter().any(|hash| hash.algorithm == algorithm)) + } + } +} diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index 6c2ad6c81..ab74423ee 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -51,6 +51,7 @@ pub use crate::direct_url::*; pub use crate::editable::*; pub use crate::error::*; pub use crate::file::*; +pub use crate::hashed::*; pub use crate::id::*; pub use crate::index_url::*; pub use crate::installed::*; @@ -66,6 +67,7 @@ mod direct_url; mod editable; mod error; mod file; +mod hashed; mod id; mod index_url; mod installed; diff --git a/crates/distribution-types/src/prioritized_distribution.rs b/crates/distribution-types/src/prioritized_distribution.rs index dc67a0f34..ac793de84 100644 --- a/crates/distribution-types/src/prioritized_distribution.rs +++ b/crates/distribution-types/src/prioritized_distribution.rs @@ -84,6 +84,8 @@ impl Display for IncompatibleDist { IncompatibleWheel::RequiresPython(python) => { write!(f, "it requires at python {python}") } + IncompatibleWheel::MissingHash => f.write_str("it has no hash"), + IncompatibleWheel::MismatchedHash => f.write_str("the hash does not match"), }, Self::Source(incompatibility) => match incompatibility { IncompatibleSource::NoBuild => { @@ -104,6 +106,8 @@ impl Display for IncompatibleDist { IncompatibleSource::RequiresPython(python) => { write!(f, "it requires python {python}") } + IncompatibleSource::MissingHash => f.write_str("it has no hash"), + IncompatibleSource::MismatchedHash => f.write_str("the hash does not match"), }, Self::Unavailable => f.write_str("no distributions are available"), } @@ -122,6 +126,8 @@ pub enum IncompatibleWheel { Tag(IncompatibleTag), RequiresPython(VersionSpecifiers), Yanked(Yanked), + MissingHash, + MismatchedHash, NoBinary, } @@ -136,6 +142,8 @@ pub enum IncompatibleSource { ExcludeNewer(Option), RequiresPython(VersionSpecifiers), Yanked(Yanked), + MissingHash, + MismatchedHash, NoBuild, } @@ -381,20 +389,26 @@ impl IncompatibleSource { Self::ExcludeNewer(timestamp_self) => match other { // Smaller timestamps are closer to the cut-off time Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self, - Self::NoBuild | Self::RequiresPython(_) | Self::Yanked(_) => true, + Self::NoBuild + | Self::RequiresPython(_) + | Self::Yanked(_) + | Self::MissingHash + | Self::MismatchedHash => true, }, Self::RequiresPython(_) => match other { Self::ExcludeNewer(_) => false, // Version specifiers cannot be reasonably compared Self::RequiresPython(_) => false, - Self::NoBuild | Self::Yanked(_) => true, + Self::NoBuild | Self::Yanked(_) | Self::MissingHash | Self::MismatchedHash => true, }, Self::Yanked(_) => match other { Self::ExcludeNewer(_) | Self::RequiresPython(_) => false, // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), - Self::NoBuild => true, + Self::NoBuild | Self::MissingHash | Self::MismatchedHash => true, }, + Self::MissingHash => false, + Self::MismatchedHash => false, Self::NoBuild => false, } } @@ -412,26 +426,37 @@ impl IncompatibleWheel { timestamp_other < timestamp_self } }, - Self::NoBinary | Self::RequiresPython(_) | Self::Tag(_) | Self::Yanked(_) => true, + Self::NoBinary + | Self::RequiresPython(_) + | Self::Tag(_) + | Self::Yanked(_) + | Self::MissingHash + | Self::MismatchedHash => true, }, Self::Tag(tag_self) => match other { Self::ExcludeNewer(_) => false, Self::Tag(tag_other) => tag_other > tag_self, - Self::NoBinary | Self::RequiresPython(_) | Self::Yanked(_) => true, + Self::NoBinary + | Self::RequiresPython(_) + | Self::Yanked(_) + | Self::MissingHash + | Self::MismatchedHash => true, }, Self::RequiresPython(_) => match other { Self::ExcludeNewer(_) | Self::Tag(_) => false, // Version specifiers cannot be reasonably compared Self::RequiresPython(_) => false, - Self::NoBinary | Self::Yanked(_) => true, + Self::NoBinary | Self::Yanked(_) | Self::MissingHash | Self::MismatchedHash => true, }, Self::Yanked(_) => match other { Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_) => false, // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), - Self::NoBinary => true, + Self::NoBinary | Self::MissingHash | Self::MismatchedHash => true, }, Self::NoBinary => false, + Self::MismatchedHash => false, + Self::MissingHash => false, } } } diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index d37eff711..c8091af7d 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -72,7 +72,7 @@ pub enum Pep508ErrorSource { String(String), /// A URL parsing error. #[error(transparent)] - UrlError(#[from] verbatim_url::VerbatimUrlError), + UrlError(#[from] VerbatimUrlError), /// The version requirement is not supported. #[error("{0}")] UnsupportedRequirement(String), diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index d2e5b3c41..ec054e83c 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -594,12 +594,12 @@ pub enum CacheBucket { impl CacheBucket { fn to_str(self) -> &'static str { match self { - Self::BuiltWheels => "built-wheels-v2", + Self::BuiltWheels => "built-wheels-v3", Self::FlatIndex => "flat-index-v0", Self::Git => "git-v0", Self::Interpreter => "interpreter-v0", Self::Simple => "simple-v7", - Self::Wheels => "wheels-v0", + Self::Wheels => "wheels-v1", Self::Archive => "archive-v0", } } diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index f7db9eebb..25ec0d8d1 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -11,14 +11,14 @@ install-wheel-rs = { workspace = true } pep440_rs = { workspace = true } pep508_rs = { workspace = true } platform-tags = { workspace = true } +pypi-types = { workspace = true } uv-auth = { workspace = true } uv-cache = { workspace = true } +uv-configuration = { workspace = true } uv-fs = { workspace = true, features = ["tokio"] } uv-normalize = { workspace = true } -uv-configuration = { workspace = true } uv-version = { workspace = true } uv-warnings = { workspace = true } -pypi-types = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } diff --git a/crates/uv-client/src/cached_client.rs b/crates/uv-client/src/cached_client.rs index a80726e8b..672785328 100644 --- a/crates/uv-client/src/cached_client.rs +++ b/crates/uv-client/src/cached_client.rs @@ -299,6 +299,34 @@ impl CachedClient { } } + /// Make a request without checking whether the cache is fresh. + pub async fn skip_cache< + Payload: Serialize + DeserializeOwned + Send + 'static, + CallBackError, + Callback, + CallbackReturn, + >( + &self, + req: Request, + cache_entry: &CacheEntry, + response_callback: Callback, + ) -> Result> + where + Callback: FnOnce(Response) -> CallbackReturn + Send, + CallbackReturn: Future> + Send, + { + let (response, cache_policy) = self.fresh_request(req).await?; + + let payload = self + .run_response_callback(cache_entry, cache_policy, response, move |resp| async { + let payload = response_callback(resp).await?; + Ok(SerdeCacheable { inner: payload }) + }) + .await?; + + Ok(payload) + } + async fn resend_and_heal_cache( &self, req: Request, diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index facd5dbc0..623e9b26e 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -2,15 +2,12 @@ use std::path::PathBuf; use futures::{FutureExt, StreamExt}; use reqwest::Response; - use tracing::{debug, info_span, warn, Instrument}; use url::Url; use distribution_filename::DistFilename; use distribution_types::{File, FileLocation, FlatIndexLocation, IndexUrl}; - use pep508_rs::VerbatimUrl; - use uv_cache::{Cache, CacheBucket}; use crate::cached_client::{CacheControl, CachedClientError}; diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index 969ee18c6..df3d62187 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch; use uv_installer::SitePackages; use uv_interpreter::PythonEnvironment; use uv_resolver::{FlatIndex, InMemoryIndex, Manifest, Options, Resolver}; -use uv_types::{BuildIsolation, InFlight}; +use uv_types::{BuildIsolation, InFlight, RequiredHashes}; #[derive(ValueEnum, Default, Clone)] pub(crate) enum ResolveCliFormat { @@ -58,6 +58,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { let index_locations = IndexLocations::new(args.index_url, args.extra_index_url, args.find_links, false); let index = InMemoryIndex::default(); + let hashes = RequiredHashes::default(); let in_flight = InFlight::default(); let no_build = if args.no_build { NoBuild::All @@ -73,6 +74,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { FlatIndex::from_entries( entries, venv.interpreter().tags()?, + &RequiredHashes::default(), &no_build, &NoBinary::None, ) @@ -107,6 +109,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { &client, &flat_index, &index, + &hashes, &build_dispatch, &site_packages, )?; diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index b35961d54..fa8ea31b4 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -21,7 +21,7 @@ use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, Reinstall, use uv_installer::{Downloader, Installer, Plan, Planner, SitePackages}; use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_resolver::{FlatIndex, InMemoryIndex, Manifest, Options, Resolver}; -use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, InFlight}; +use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, InFlight, RequiredHashes}; /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] /// documentation. @@ -134,6 +134,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { async fn resolve<'data>(&'data self, requirements: &'data [Requirement]) -> Result { let markers = self.interpreter.markers(); let tags = self.interpreter.tags()?; + let hashes = RequiredHashes::default(); let resolver = Resolver::new( Manifest::simple(requirements.to_vec()), self.options, @@ -143,6 +144,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.client, self.flat_index, self.index, + &hashes, self, &EmptyInstalledPackages, )?; @@ -176,6 +178,9 @@ impl<'a> BuildContext for BuildDispatch<'a> { venv.root().display(), ); + // Don't enforce hashes for build dependencies. + let hashes = RequiredHashes::default(); + // Determine the current environment markers. let tags = self.interpreter.tags()?; @@ -192,6 +197,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { site_packages, &Reinstall::None, &NoBinary::None, + &RequiredHashes::default(), self.index_locations, self.cache(), venv, @@ -220,7 +226,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { vec![] } else { // TODO(konstin): Check that there is no endless recursion. - let downloader = Downloader::new(self.cache, tags, self.client, self); + let downloader = Downloader::new(self.cache, tags, &hashes, self.client, self); debug!( "Downloading and building requirement{} for build: {}", if remote.len() == 1 { "" } else { "s" }, diff --git a/crates/uv-distribution/Cargo.toml b/crates/uv-distribution/Cargo.toml index 53d244433..a4814ff6d 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -33,6 +33,7 @@ uv-configuration = { workspace = true } anyhow = { workspace = true } fs-err = { workspace = true } futures = { workspace = true } +md-5 = { workspace = true } nanoid = { workspace = true } once_cell = { workspace = true } reqwest = { workspace = true } @@ -40,6 +41,7 @@ reqwest-middleware = { workspace = true } rmp-serde = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, features = ["derive"] } +sha2 = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/crates/uv-distribution/src/archive.rs b/crates/uv-distribution/src/archive.rs new file mode 100644 index 000000000..a53de619d --- /dev/null +++ b/crates/uv-distribution/src/archive.rs @@ -0,0 +1,36 @@ +use std::path::PathBuf; + +use distribution_types::Hashed; +use pypi_types::HashDigest; + +/// An archive (unzipped wheel) that exists in the local cache. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct Archive { + /// The path to the archive entry in the wheel's archive bucket. + pub path: PathBuf, + /// The computed hashes of the archive. + pub hashes: Vec, +} + +impl Archive { + /// Create a new [`Archive`] with the given path and hashes. + pub(crate) fn new(path: PathBuf, hashes: Vec) -> Self { + Self { path, hashes } + } + + /// Return the path to the archive entry in the wheel's archive bucket. + pub fn path(&self) -> &PathBuf { + &self.path + } + + /// Return the computed hashes of the archive. + pub fn hashes(&self) -> &[HashDigest] { + &self.hashes + } +} + +impl Hashed for Archive { + fn hashes(&self) -> &[HashDigest] { + &self.hashes + } +} diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 6ece58e51..0db62a92d 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -11,16 +11,19 @@ use url::Url; use distribution_filename::WheelFilename; use distribution_types::{ - BuildableSource, BuiltDist, Dist, FileLocation, IndexLocations, LocalEditable, Name, SourceDist, + BuildableSource, BuiltDist, Dist, FileLocation, Hashed, IndexLocations, LocalEditable, Name, + SourceDist, }; use platform_tags::Tags; -use pypi_types::Metadata23; +use pypi_types::{HashDigest, Metadata23}; use uv_cache::{ArchiveTimestamp, CacheBucket, CacheEntry, CachedByTimestamp, WheelCache}; use uv_client::{CacheControl, CachedClientError, Connectivity, RegistryClient}; use uv_configuration::{NoBinary, NoBuild}; +use uv_extract::hash::Hasher; use uv_fs::write_atomic; use uv_types::BuildContext; +use crate::archive::Archive; use crate::locks::Locks; use crate::{Error, LocalWheel, Reporter, SourceDistributionBuilder}; @@ -79,28 +82,38 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> /// Either fetch the wheel or fetch and build the source distribution /// - /// If `no_remote_wheel` is set, the wheel will be built from a source distribution - /// even if compatible pre-built wheels are available. + /// Returns a wheel that's compliant with the given platform tags. + /// + /// While hashes will be generated in some cases, hash-checking is only enforced for source + /// distributions, and should be enforced by the caller for wheels. #[instrument(skip_all, fields(%dist))] - pub async fn get_or_build_wheel(&self, dist: &Dist, tags: &Tags) -> Result { + pub async fn get_or_build_wheel( + &self, + dist: &Dist, + tags: &Tags, + hashes: &[HashDigest], + ) -> Result { match dist { - Dist::Built(built) => self.get_wheel(built).await, - Dist::Source(source) => self.build_wheel(source, tags).await, + Dist::Built(built) => self.get_wheel(built, hashes).await, + Dist::Source(source) => self.build_wheel(source, tags, hashes).await, } } /// Either fetch the only wheel metadata (directly from the index or with range requests) or /// fetch and build the source distribution. /// - /// Returns the [`Metadata23`], along with a "precise" URL for the source distribution, if - /// possible. For example, given a Git dependency with a reference to a branch or tag, return a - /// URL with a precise reference to the current commit of that branch or tag. + /// While hashes will be generated in some cases, hash-checking is only enforced for source + /// distributions, and should be enforced by the caller for wheels. #[instrument(skip_all, fields(%dist))] - pub async fn get_or_build_wheel_metadata(&self, dist: &Dist) -> Result { + pub async fn get_or_build_wheel_metadata( + &self, + dist: &Dist, + hashes: &[HashDigest], + ) -> Result { match dist { - Dist::Built(built) => self.get_wheel_metadata(built).await, + Dist::Built(built) => self.get_wheel_metadata(built, hashes).await, Dist::Source(source) => { - self.build_wheel_metadata(&BuildableSource::Dist(source)) + self.build_wheel_metadata(&BuildableSource::Dist(source), hashes) .await } } @@ -118,7 +131,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .build_editable(editable, editable_wheel_dir) .await?; - // Unzip. + // Unzip into the editable wheel directory. let path = editable_wheel_dir.join(&disk_filename); let target = editable_wheel_dir.join(cache_key::digest(&editable.path)); let archive = self.unzip_wheel(&path, &target).await?; @@ -126,13 +139,21 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> dist, filename, archive, + hashes: vec![], }; Ok((wheel, metadata)) } /// Fetch a wheel from the cache or download it from the index. - async fn get_wheel(&self, dist: &BuiltDist) -> Result { + /// + /// While hashes will be generated in some cases, hash-checking is _not_ enforced and should + /// instead be enforced by the caller. + async fn get_wheel( + &self, + dist: &BuiltDist, + hashes: &[HashDigest], + ) -> Result { let no_binary = match self.build_context.no_binary() { NoBinary::None => false, NoBinary::All => true, @@ -157,8 +178,9 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> WheelCache::Index(&wheel.index).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); + return self - .load_wheel(path, &wheel.filename, cache_entry, dist) + .load_wheel(path, &wheel.filename, cache_entry, dist, hashes) .await; } }; @@ -172,12 +194,13 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> // Download and unzip. match self - .stream_wheel(url.clone(), &wheel.filename, &wheel_entry, dist) + .stream_wheel(url.clone(), &wheel.filename, &wheel_entry, dist, hashes) .await { Ok(archive) => Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive, + archive: archive.path, + hashes: archive.hashes, filename: wheel.filename.clone(), }), Err(Error::Extract(err)) if err.is_http_streaming_unsupported() => { @@ -188,11 +211,12 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> // If the request failed because streaming is unsupported, download the // wheel directly. let archive = self - .download_wheel(url, &wheel.filename, &wheel_entry, dist) + .download_wheel(url, &wheel.filename, &wheel_entry, dist, hashes) .await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive, + archive: archive.path, + hashes: archive.hashes, filename: wheel.filename.clone(), }) } @@ -210,12 +234,19 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> // Download and unzip. match self - .stream_wheel(wheel.url.raw().clone(), &wheel.filename, &wheel_entry, dist) + .stream_wheel( + wheel.url.raw().clone(), + &wheel.filename, + &wheel_entry, + dist, + hashes, + ) .await { Ok(archive) => Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive, + archive: archive.path, + hashes: archive.hashes, filename: wheel.filename.clone(), }), Err(Error::Client(err)) if err.is_http_streaming_unsupported() => { @@ -231,11 +262,13 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> &wheel.filename, &wheel_entry, dist, + hashes, ) .await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive, + archive: archive.path, + hashes: archive.hashes, filename: wheel.filename.clone(), }) } @@ -249,7 +282,8 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); - self.load_wheel(&wheel.path, &wheel.filename, cache_entry, dist) + + self.load_wheel(&wheel.path, &wheel.filename, cache_entry, dist, hashes) .await } } @@ -257,24 +291,33 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> /// Convert a source distribution into a wheel, fetching it from the cache or building it if /// necessary. - async fn build_wheel(&self, dist: &SourceDist, tags: &Tags) -> Result { + /// + /// The returned wheel is guaranteed to come from a distribution with a matching hash, and + /// no build processes will be executed for distributions with mismatched hashes. + async fn build_wheel( + &self, + dist: &SourceDist, + tags: &Tags, + hashes: &[HashDigest], + ) -> Result { let lock = self.locks.acquire(&Dist::Source(dist.clone())).await; let _guard = lock.lock().await; let built_wheel = self .builder - .download_and_build(&BuildableSource::Dist(dist), tags) + .download_and_build(&BuildableSource::Dist(dist), tags, hashes) .boxed() .await?; // If the wheel was unzipped previously, respect it. Source distributions are - // cached under a unique build ID, so unzipped directories are never stale. + // cached under a unique revision ID, so unzipped directories are never stale. match built_wheel.target.canonicalize() { Ok(archive) => { return Ok(LocalWheel { dist: Dist::Source(dist.clone()), archive, filename: built_wheel.filename, + hashes: built_wheel.hashes, }); } Err(err) if err.kind() == io::ErrorKind::NotFound => {} @@ -287,12 +330,20 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> archive: self .unzip_wheel(&built_wheel.path, &built_wheel.target) .await?, + hashes: built_wheel.hashes, filename: built_wheel.filename, }) } /// Fetch the wheel metadata from the index, or from the cache if possible. - pub async fn get_wheel_metadata(&self, dist: &BuiltDist) -> Result { + /// + /// While hashes will be generated in some cases, hash-checking is _not_ enforced and should + /// instead be enforced by the caller. + pub async fn get_wheel_metadata( + &self, + dist: &BuiltDist, + hashes: &[HashDigest], + ) -> Result { match self.client.wheel_metadata(dist).boxed().await { Ok(metadata) => Ok(metadata), Err(err) if err.is_http_streaming_unsupported() => { @@ -300,7 +351,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> // If the request failed due to an error that could be resolved by // downloading the wheel directly, try that. - let wheel = self.get_wheel(dist).await?; + let wheel = self.get_wheel(dist, hashes).await?; Ok(wheel.metadata()?) } Err(err) => Err(err.into()), @@ -308,9 +359,13 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> } /// Build the wheel metadata for a source distribution, or fetch it from the cache if possible. + /// + /// The returned metadata is guaranteed to come from a distribution with a matching hash, and + /// no build processes will be executed for distributions with mismatched hashes. pub async fn build_wheel_metadata( &self, source: &BuildableSource<'_>, + hashes: &[HashDigest], ) -> Result { let no_build = match self.build_context.no_build() { NoBuild::All => true, @@ -330,7 +385,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let metadata = self .builder - .download_and_build_metadata(source) + .download_and_build_metadata(source, hashes) .boxed() .await?; Ok(metadata) @@ -343,7 +398,8 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> filename: &WheelFilename, wheel_entry: &CacheEntry, dist: &BuiltDist, - ) -> Result { + hashes: &[HashDigest], + ) -> Result { // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.stem())); @@ -354,23 +410,42 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .map_err(|err| self.handle_response_errors(err)) .into_async_read(); + // Create a hasher for each hash algorithm. + let algorithms = { + let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); + hash.sort(); + hash.dedup(); + hash + }; + let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); + let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers); + // Download and unzip the wheel to a temporary directory. let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) .map_err(Error::CacheWrite)?; - uv_extract::stream::unzip(reader.compat(), temp_dir.path()).await?; + uv_extract::stream::unzip(&mut hasher, temp_dir.path()).await?; + + // If necessary, exhaust the reader to compute the hash. + if !hashes.is_empty() { + hasher.finish().await.map_err(Error::HashExhaustion)?; + } // Persist the temporary directory to the directory store. - let archive = self + let path = self .build_context .cache() .persist(temp_dir.into_path(), wheel_entry.path()) .await .map_err(Error::CacheRead)?; - Ok(archive) + Ok(Archive::new( + path, + hashers.into_iter().map(HashDigest::from).collect(), + )) } .instrument(info_span!("wheel", wheel = %dist)) }; + // Fetch the archive from the cache, or download it if necessary. let req = self.request(url.clone())?; let cache_control = match self.client.connectivity() { Connectivity::Online => CacheControl::from( @@ -391,6 +466,20 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> CachedClientError::Client(err) => Error::Client(err), })?; + // If the archive is missing the required hashes, force a refresh. + let archive = if archive.has_digests(hashes) { + archive + } else { + self.client + .cached_client() + .skip_cache(self.request(url)?, &http_entry, download) + .await + .map_err(|err| match err { + CachedClientError::Callback(err) => err, + CachedClientError::Client(err) => Error::Client(err), + })? + }; + Ok(archive) } @@ -401,7 +490,8 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> filename: &WheelFilename, wheel_entry: &CacheEntry, dist: &BuiltDist, - ) -> Result { + hashes: &[HashDigest], + ) -> Result { // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.stem())); @@ -427,16 +517,48 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> file.seek(io::SeekFrom::Start(0)) .await .map_err(Error::CacheWrite)?; - uv_extract::seek::unzip(file, temp_dir.path()).await?; + + // If no hashes are required, parallelize the unzip operation. + let hashes = if hashes.is_empty() { + let file = file.into_std().await; + tokio::task::spawn_blocking({ + let target = temp_dir.path().to_owned(); + move || -> Result<(), uv_extract::Error> { + // Unzip the wheel into a temporary directory. + uv_extract::unzip(file, &target)?; + Ok(()) + } + }) + .await??; + + vec![] + } else { + // Create a hasher for each hash algorithm. + let algorithms = { + let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); + hash.sort(); + hash.dedup(); + hash + }; + let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); + let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers); + uv_extract::stream::unzip(&mut hasher, temp_dir.path()).await?; + + // If necessary, exhaust the reader to compute the hash. + hasher.finish().await.map_err(Error::HashExhaustion)?; + + hashers.into_iter().map(HashDigest::from).collect() + }; // Persist the temporary directory to the directory store. - let archive = self + let path = self .build_context .cache() .persist(temp_dir.into_path(), wheel_entry.path()) .await .map_err(Error::CacheRead)?; - Ok(archive) + + Ok(Archive::new(path, hashes)) } .instrument(info_span!("wheel", wheel = %dist)) }; @@ -451,7 +573,6 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> ), Connectivity::Offline => CacheControl::AllowStale, }; - let archive = self .client .cached_client() @@ -462,6 +583,20 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> CachedClientError::Client(err) => Error::Client(err), })?; + // If the archive is missing the required hashes, force a refresh. + let archive = if archive.has_digests(hashes) { + archive + } else { + self.client + .cached_client() + .skip_cache(self.request(url)?, &http_entry, download) + .await + .map_err(|err| match err { + CachedClientError::Callback(err) => err, + CachedClientError::Client(err) => Error::Client(err), + })? + }; + Ok(archive) } @@ -472,6 +607,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> filename: &WheelFilename, wheel_entry: CacheEntry, dist: &BuiltDist, + hashes: &[HashDigest], ) -> Result { // Determine the last-modified time of the wheel. let modified = ArchiveTimestamp::from_file(path).map_err(Error::CacheRead)?; @@ -481,20 +617,66 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let archive = read_timestamped_archive(&archive_entry, modified)?; // If the file is already unzipped, and the cache is up-to-date, return it. - if let Some(archive) = archive { + if let Some(archive) = archive.filter(|archive| archive.has_digests(hashes)) { Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive, + archive: archive.path, + hashes: archive.hashes, filename: filename.clone(), }) - } else { + } else if hashes.is_empty() { // Otherwise, unzip the wheel. - let archive = self.unzip_wheel(path, wheel_entry.path()).await?; + let archive = Archive::new(self.unzip_wheel(path, wheel_entry.path()).await?, vec![]); write_timestamped_archive(&archive_entry, archive.clone(), modified).await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive, + archive: archive.path, + hashes: archive.hashes, + filename: filename.clone(), + }) + } else { + // If necessary, compute the hashes of the wheel. + let file = fs_err::tokio::File::open(path) + .await + .map_err(Error::CacheRead)?; + let temp_dir = tempfile::tempdir_in(self.build_context.cache().root()) + .map_err(Error::CacheWrite)?; + + // Create a hasher for each hash algorithm. + let algorithms = { + let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); + hash.sort(); + hash.dedup(); + hash + }; + let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); + let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers); + + // Unzip the wheel to a temporary directory. + uv_extract::stream::unzip(&mut hasher, temp_dir.path()).await?; + + // Exhaust the reader to compute the hash. + hasher.finish().await.map_err(Error::HashExhaustion)?; + + // Persist the temporary directory to the directory store. + let archive = self + .build_context + .cache() + .persist(temp_dir.into_path(), wheel_entry.path()) + .await + .map_err(Error::CacheWrite)?; + + let hashes = hashers.into_iter().map(HashDigest::from).collect(); + + // Write the archive pointer to the cache. + let archive = Archive::new(archive, hashes); + write_timestamped_archive(&archive_entry, archive.clone(), modified).await?; + + Ok(LocalWheel { + dist: Dist::Built(dist.clone()), + archive: archive.path, + hashes: archive.hashes, filename: filename.clone(), }) } @@ -549,7 +731,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> /// Write a timestamped archive path to the cache. async fn write_timestamped_archive( cache_entry: &CacheEntry, - data: PathBuf, + data: Archive, modified: ArchiveTimestamp, ) -> Result<(), Error> { write_atomic( @@ -564,13 +746,13 @@ async fn write_timestamped_archive( } /// Read an existing timestamped archive path, if it exists and is up-to-date. -fn read_timestamped_archive( +pub fn read_timestamped_archive( cache_entry: &CacheEntry, modified: ArchiveTimestamp, -) -> Result, Error> { +) -> Result, Error> { match fs_err::read(cache_entry.path()) { Ok(cached) => { - let cached = rmp_serde::from_slice::>(&cached)?; + let cached = rmp_serde::from_slice::>(&cached)?; if cached.timestamp == modified.timestamp() { return Ok(Some(cached.data)); } diff --git a/crates/uv-distribution/src/download.rs b/crates/uv-distribution/src/download.rs index b7123ff7c..68db0b722 100644 --- a/crates/uv-distribution/src/download.rs +++ b/crates/uv-distribution/src/download.rs @@ -1,8 +1,8 @@ use std::path::{Path, PathBuf}; use distribution_filename::WheelFilename; -use distribution_types::{CachedDist, Dist}; -use pypi_types::Metadata23; +use distribution_types::{CachedDist, Dist, Hashed}; +use pypi_types::{HashDigest, Metadata23}; use crate::Error; @@ -16,6 +16,8 @@ pub struct LocalWheel { /// The canonicalized path in the cache directory to which the wheel was downloaded. /// Typically, a directory within the archive bucket. pub(crate) archive: PathBuf, + /// The computed hashes of the wheel. + pub(crate) hashes: Vec, } impl LocalWheel { @@ -40,10 +42,16 @@ impl LocalWheel { } } +impl Hashed for LocalWheel { + fn hashes(&self) -> &[HashDigest] { + &self.hashes + } +} + /// Convert a [`LocalWheel`] into a [`CachedDist`]. impl From for CachedDist { fn from(wheel: LocalWheel) -> CachedDist { - CachedDist::from_remote(wheel.dist, wheel.filename, wheel.archive) + CachedDist::from_remote(wheel.dist, wheel.filename, wheel.hashes, wheel.archive) } } diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index 42bf4f48b..d5a77f81a 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -4,6 +4,7 @@ use zip::result::ZipError; use distribution_filename::WheelFilenameError; use pep440_rs::Version; +use pypi_types::HashDigest; use uv_client::BetterReqwestError; use uv_normalize::PackageName; @@ -81,6 +82,23 @@ pub enum Error { /// Should not occur; only seen when another task panicked. #[error("The task executor is broken, did some other task panic?")] Join(#[from] JoinError), + + /// An I/O error that occurs while exhausting a reader to compute a hash. + #[error("Failed to hash distribution")] + HashExhaustion(#[source] std::io::Error), + + #[error("Hash mismatch for {distribution}\n\nExpected:\n{expected}\n\nComputed:\n{actual}")] + HashMismatch { + distribution: String, + expected: String, + actual: String, + }, + + #[error("Hash-checking is not supported for local directories: {0}")] + HashesNotSupportedSourceTree(String), + + #[error("Hash-checking is not supported for Git repositories: {0}")] + HashesNotSupportedGit(String), } impl From for Error { @@ -99,3 +117,30 @@ impl From for Error { } } } + +impl Error { + /// Construct a hash mismatch error. + pub fn hash_mismatch( + distribution: String, + expected: &[HashDigest], + actual: &[HashDigest], + ) -> Error { + let expected = expected + .iter() + .map(|hash| format!(" {hash}")) + .collect::>() + .join("\n"); + + let actual = actual + .iter() + .map(|hash| format!(" {hash}")) + .collect::>() + .join("\n"); + + Self::HashMismatch { + distribution, + expected, + actual, + } + } +} diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index ca4f59b45..ea7fd1df6 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -1,7 +1,10 @@ -use distribution_types::{git_reference, DirectUrlSourceDist, GitSourceDist, PathSourceDist}; +use distribution_types::{ + git_reference, DirectUrlSourceDist, GitSourceDist, Hashed, PathSourceDist, +}; use platform_tags::Tags; use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, CacheShard, WheelCache}; use uv_fs::symlinks; +use uv_types::RequiredHashes; use crate::index::cached_wheel::CachedWheel; use crate::source::{read_http_revision, read_timestamped_revision, REVISION}; @@ -12,12 +15,17 @@ use crate::Error; pub struct BuiltWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, + hashes: &'a RequiredHashes, } impl<'a> BuiltWheelIndex<'a> { /// Initialize an index of built distributions. - pub fn new(cache: &'a Cache, tags: &'a Tags) -> Self { - Self { cache, tags } + pub fn new(cache: &'a Cache, tags: &'a Tags, hashes: &'a RequiredHashes) -> Self { + Self { + cache, + tags, + hashes, + } } /// Return the most compatible [`CachedWheel`] for a given source distribution at a direct URL. @@ -31,13 +39,19 @@ impl<'a> BuiltWheelIndex<'a> { WheelCache::Url(source_dist.url.raw()).root(), ); - // Read the revision from the cache. There's no need to enforce freshness, since we - // enforce freshness on the entries. + // Read the revision from the cache. let revision_entry = cache_shard.entry(REVISION); let Some(revision) = read_http_revision(&revision_entry)? else { return Ok(None); }; + // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. + if let Some(hashes) = self.hashes.get(&source_dist.name) { + if !revision.satisfies(hashes) { + return Ok(None); + } + } + Ok(self.find(&cache_shard.shard(revision.id()))) } @@ -55,18 +69,29 @@ impl<'a> BuiltWheelIndex<'a> { return Err(Error::DirWithoutEntrypoint); }; - // Read the revision from the cache. There's no need to enforce freshness, since we - // enforce freshness on the entries. + // Read the revision from the cache. let revision_entry = cache_shard.entry(REVISION); let Some(revision) = read_timestamped_revision(&revision_entry, modified)? else { return Ok(None); }; + // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. + if let Some(hashes) = self.hashes.get(&source_dist.name) { + if !revision.satisfies(hashes) { + return Ok(None); + } + } + Ok(self.find(&cache_shard.shard(revision.id()))) } /// Return the most compatible [`CachedWheel`] for a given source distribution at a git URL. pub fn git(&self, source_dist: &GitSourceDist) -> Option { + // Enforce hash-checking, which isn't supported for Git distributions. + if self.hashes.get(&source_dist.name).is_some() { + return None; + } + let Ok(Some(git_sha)) = git_reference(&source_dist.url) else { return None; }; @@ -100,7 +125,7 @@ impl<'a> BuiltWheelIndex<'a> { // Unzipped wheels are stored as symlinks into the archive directory. for subdir in symlinks(shard) { - match CachedWheel::from_path(&subdir) { + match CachedWheel::from_built_source(&subdir) { None => {} Some(dist_info) => { // Pick the wheel with the highest priority diff --git a/crates/uv-distribution/src/index/cached_wheel.rs b/crates/uv-distribution/src/index/cached_wheel.rs index a8e4172aa..157a4cffa 100644 --- a/crates/uv-distribution/src/index/cached_wheel.rs +++ b/crates/uv-distribution/src/index/cached_wheel.rs @@ -1,9 +1,13 @@ use std::path::Path; use distribution_filename::WheelFilename; -use distribution_types::{CachedDirectUrlDist, CachedRegistryDist}; +use distribution_types::{CachedDirectUrlDist, CachedRegistryDist, Hashed}; use pep508_rs::VerbatimUrl; -use uv_cache::CacheEntry; +use pypi_types::HashDigest; +use uv_cache::{CacheEntry, CachedByTimestamp}; +use uv_client::DataWithCachePolicy; + +use crate::archive::Archive; #[derive(Debug, Clone)] pub struct CachedWheel { @@ -11,16 +15,23 @@ pub struct CachedWheel { pub filename: WheelFilename, /// The [`CacheEntry`] for the wheel. pub entry: CacheEntry, + /// The [`HashDigest`]s for the wheel. + pub hashes: Vec, } impl CachedWheel { /// Try to parse a distribution from a cached directory name (like `typing-extensions-4.8.0-py3-none-any`). - pub fn from_path(path: &Path) -> Option { + pub fn from_built_source(path: &Path) -> Option { let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_stem(filename).ok()?; let archive = path.canonicalize().ok()?; let entry = CacheEntry::from_path(archive); - Some(Self { filename, entry }) + let hashes = Vec::new(); + Some(Self { + filename, + entry, + hashes, + }) } /// Convert a [`CachedWheel`] into a [`CachedRegistryDist`]. @@ -28,6 +39,7 @@ impl CachedWheel { CachedRegistryDist { filename: self.filename, path: self.entry.into_path_buf(), + hashes: self.hashes, } } @@ -38,6 +50,56 @@ impl CachedWheel { url, path: self.entry.into_path_buf(), editable: false, + hashes: self.hashes, } } + + /// Read a cached wheel from a `.http` pointer (e.g., `anyio-4.0.0-py3-none-any.http`). + pub fn from_http_pointer(path: &Path) -> Option { + // Determine the wheel filename. + let filename = path.file_name()?.to_str()?; + let filename = WheelFilename::from_stem(filename).ok()?; + + // Read the pointer. + let file = fs_err::File::open(path).ok()?; + let data = DataWithCachePolicy::from_reader(file).ok()?.data; + let archive = rmp_serde::from_slice::(&data).ok()?; + + // Convert to a cached wheel. + let entry = CacheEntry::from_path(archive.path); + let hashes = archive.hashes; + Some(Self { + filename, + entry, + hashes, + }) + } + + /// Read a cached wheel from a `.rev` pointer (e.g., `anyio-4.0.0-py3-none-any.rev`). + pub fn from_revision_pointer(path: &Path) -> Option { + // Determine the wheel filename. + let filename = path.file_name()?.to_str()?; + let filename = WheelFilename::from_stem(filename).ok()?; + + // Read the pointer. + let cached = fs_err::read(path).ok()?; + let archive = rmp_serde::from_slice::>(&cached) + .ok()? + .data; + + // Convert to a cached wheel. + let entry = CacheEntry::from_path(archive.path); + let hashes = archive.hashes; + Some(Self { + filename, + entry, + hashes, + }) + } +} + +impl Hashed for CachedWheel { + fn hashes(&self) -> &[HashDigest] { + &self.hashes + } } diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index d1dbf251f..34e5a85c3 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -1,16 +1,16 @@ use std::collections::hash_map::Entry; use std::collections::BTreeMap; -use std::path::Path; use rustc_hash::FxHashMap; -use distribution_types::{CachedRegistryDist, FlatIndexLocation, IndexLocations, IndexUrl}; +use distribution_types::{CachedRegistryDist, FlatIndexLocation, Hashed, IndexLocations, IndexUrl}; use pep440_rs::Version; use pep508_rs::VerbatimUrl; use platform_tags::Tags; use uv_cache::{Cache, CacheBucket, WheelCache}; -use uv_fs::{directories, symlinks}; +use uv_fs::{directories, files, symlinks}; use uv_normalize::PackageName; +use uv_types::RequiredHashes; use crate::index::cached_wheel::CachedWheel; use crate::source::{read_http_revision, REVISION}; @@ -21,16 +21,23 @@ pub struct RegistryWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, index_locations: &'a IndexLocations, + hashes: &'a RequiredHashes, index: FxHashMap<&'a PackageName, BTreeMap>, } impl<'a> RegistryWheelIndex<'a> { /// Initialize an index of registry distributions. - pub fn new(cache: &'a Cache, tags: &'a Tags, index_locations: &'a IndexLocations) -> Self { + pub fn new( + cache: &'a Cache, + tags: &'a Tags, + index_locations: &'a IndexLocations, + hashes: &'a RequiredHashes, + ) -> Self { Self { cache, tags, index_locations, + hashes, index: FxHashMap::default(), } } @@ -65,6 +72,7 @@ impl<'a> RegistryWheelIndex<'a> { self.cache, self.tags, self.index_locations, + self.hashes, )), }; versions @@ -76,8 +84,10 @@ impl<'a> RegistryWheelIndex<'a> { cache: &Cache, tags: &Tags, index_locations: &IndexLocations, + hashes: &RequiredHashes, ) -> BTreeMap { let mut versions = BTreeMap::new(); + let hashes = hashes.get(package).unwrap_or_default(); // Collect into owned `IndexUrl` let flat_index_urls: Vec = index_locations @@ -100,7 +110,34 @@ impl<'a> RegistryWheelIndex<'a> { WheelCache::Index(index_url).wheel_dir(package.to_string()), ); - Self::add_directory(&wheel_dir, tags, &mut versions); + // For registry wheels, the cache structure is: `//.http` + // or `///.rev`. + for file in files(&wheel_dir) { + if file + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("http")) + { + if let Some(wheel) = CachedWheel::from_http_pointer(&wheel_dir.join(&file)) { + // Enforce hash-checking based on the built distribution. + if wheel.satisfies(hashes) { + Self::add_wheel(wheel, tags, &mut versions); + } + } + } + + if file + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("rev")) + { + if let Some(wheel) = CachedWheel::from_revision_pointer(&wheel_dir.join(&file)) + { + // Enforce hash-checking based on the built distribution. + if wheel.satisfies(hashes) { + Self::add_wheel(wheel, tags, &mut versions); + } + } + } + } // Index all the built wheels, created by downloading and building source distributions // from the registry. @@ -115,7 +152,14 @@ impl<'a> RegistryWheelIndex<'a> { let cache_shard = cache_shard.shard(shard); let revision_entry = cache_shard.entry(REVISION); if let Ok(Some(revision)) = read_http_revision(&revision_entry) { - Self::add_directory(cache_shard.join(revision.id()), tags, &mut versions); + // Enforce hash-checking based on the source distribution. + if revision.satisfies(hashes) { + for wheel_dir in symlinks(cache_shard.join(revision.id())) { + if let Some(wheel) = CachedWheel::from_built_source(&wheel_dir) { + Self::add_wheel(wheel, tags, &mut versions); + } + } + } }; } } @@ -123,33 +167,23 @@ impl<'a> RegistryWheelIndex<'a> { versions } - /// Add the wheels in a given directory to the index. - /// - /// Each subdirectory in the given path is expected to be that of an unzipped wheel. - fn add_directory( - path: impl AsRef, + /// Add the [`CachedWheel`] to the index. + fn add_wheel( + wheel: CachedWheel, tags: &Tags, versions: &mut BTreeMap, ) { - // Unzipped wheels are stored as symlinks into the archive directory. - for wheel_dir in symlinks(path.as_ref()) { - match CachedWheel::from_path(&wheel_dir) { - None => {} - Some(dist_info) => { - let dist_info = dist_info.into_registry_dist(); + let dist_info = wheel.into_registry_dist(); - // Pick the wheel with the highest priority - let compatibility = dist_info.filename.compatibility(tags); - if let Some(existing) = versions.get_mut(&dist_info.filename.version) { - // Override if we have better compatibility - if compatibility > existing.filename.compatibility(tags) { - *existing = dist_info; - } - } else if compatibility.is_compatible() { - versions.insert(dist_info.filename.version.clone(), dist_info); - } - } + // Pick the wheel with the highest priority + let compatibility = dist_info.filename.compatibility(tags); + if let Some(existing) = versions.get_mut(&dist_info.filename.version) { + // Override if we have better compatibility + if compatibility > existing.filename.compatibility(tags) { + *existing = dist_info; } + } else if compatibility.is_compatible() { + versions.insert(dist_info.filename.version.clone(), dist_info); } } } diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index f74b0fc9d..61eeb41a4 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -1,4 +1,5 @@ -pub use distribution_database::DistributionDatabase; +pub use archive::Archive; +pub use distribution_database::{read_timestamped_archive, DistributionDatabase}; pub use download::LocalWheel; pub use error::Error; pub use git::{is_same_reference, to_precise}; @@ -6,6 +7,7 @@ pub use index::{BuiltWheelIndex, RegistryWheelIndex}; pub use reporter::Reporter; pub use source::SourceDistributionBuilder; +mod archive; mod distribution_database; mod download; mod error; diff --git a/crates/uv-distribution/src/source/built_wheel_metadata.rs b/crates/uv-distribution/src/source/built_wheel_metadata.rs index 3115d9882..664e32f8f 100644 --- a/crates/uv-distribution/src/source/built_wheel_metadata.rs +++ b/crates/uv-distribution/src/source/built_wheel_metadata.rs @@ -2,19 +2,23 @@ use std::path::PathBuf; use std::str::FromStr; use distribution_filename::WheelFilename; +use distribution_types::Hashed; use platform_tags::Tags; +use pypi_types::HashDigest; use uv_cache::CacheShard; use uv_fs::files; /// The information about the wheel we either just built or got from the cache. #[derive(Debug, Clone)] -pub struct BuiltWheelMetadata { +pub(crate) struct BuiltWheelMetadata { /// The path to the built wheel. pub(crate) path: PathBuf, /// The expected path to the downloaded wheel's entry in the cache. pub(crate) target: PathBuf, /// The parsed filename. pub(crate) filename: WheelFilename, + /// The computed hashes of the source distribution from which the wheel was built. + pub(crate) hashes: Vec, } impl BuiltWheelMetadata { @@ -39,6 +43,20 @@ impl BuiltWheelMetadata { target: cache_shard.join(filename.stem()), path, filename, + hashes: vec![], }) } + + /// Set the computed hashes of the wheel. + #[must_use] + pub(crate) fn with_hashes(mut self, hashes: Vec) -> Self { + self.hashes = hashes; + self + } +} + +impl Hashed for BuiltWheelMetadata { + fn hashes(&self) -> &[HashDigest] { + &self.hashes + } } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index dab44f313..deb37e753 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -16,12 +16,12 @@ use zip::ZipArchive; use distribution_filename::WheelFilename; use distribution_types::{ - BuildableSource, DirectArchiveUrl, Dist, FileLocation, GitSourceUrl, LocalEditable, + BuildableSource, DirectArchiveUrl, Dist, FileLocation, GitSourceUrl, Hashed, LocalEditable, PathSourceDist, PathSourceUrl, RemoteSource, SourceDist, SourceUrl, }; use install_wheel_rs::metadata::read_archive_metadata; use platform_tags::Tags; -use pypi_types::Metadata23; +use pypi_types::{HashDigest, Metadata23}; use uv_cache::{ ArchiveTimestamp, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Freshness, WheelCache, }; @@ -29,6 +29,7 @@ use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; use uv_configuration::{BuildKind, NoBuild}; +use uv_extract::hash::Hasher; use uv_fs::write_atomic; use uv_types::{BuildContext, SourceBuildTrait}; @@ -49,9 +50,7 @@ pub struct SourceDistributionBuilder<'a, T: BuildContext> { } /// The name of the file that contains the revision ID, encoded via `MsgPack`. -/// -/// TODO(charlie): Update the filename whenever we bump the cache version. -pub(crate) const REVISION: &str = "manifest.msgpack"; +pub(crate) const REVISION: &str = "revision.msgpack"; /// The name of the file that contains the cached distribution metadata, encoded via `MsgPack`. pub(crate) const METADATA: &str = "metadata.msgpack"; @@ -76,10 +75,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } /// Download and build a [`SourceDist`]. - pub async fn download_and_build( + pub(super) async fn download_and_build( &self, source: &BuildableSource<'_>, tags: &Tags, + hashes: &[HashDigest], ) -> Result { let built_wheel_metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { @@ -100,6 +100,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { path: Cow::Borrowed(path), }, tags, + hashes, ) .boxed() .await; @@ -115,9 +116,17 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .join(dist.filename.version.to_string()), ); - self.url(source, &dist.file.filename, &url, &cache_shard, None, tags) - .boxed() - .await? + self.url( + source, + &dist.file.filename, + &url, + &cache_shard, + None, + tags, + hashes, + ) + .boxed() + .await? } BuildableSource::Dist(SourceDist::DirectUrl(dist)) => { let filename = dist.filename().expect("Distribution must have a filename"); @@ -136,22 +145,23 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &cache_shard, subdirectory.as_deref(), tags, + hashes, ) .boxed() .await? } BuildableSource::Dist(SourceDist::Git(dist)) => { - self.git(source, &GitSourceUrl::from(dist), tags) + self.git(source, &GitSourceUrl::from(dist), tags, hashes) .boxed() .await? } BuildableSource::Dist(SourceDist::Path(dist)) => { if dist.path.is_dir() { - self.source_tree(source, &PathSourceUrl::from(dist), tags) + self.source_tree(source, &PathSourceUrl::from(dist), tags, hashes) .boxed() .await? } else { - self.archive(source, &PathSourceUrl::from(dist), tags) + self.archive(source, &PathSourceUrl::from(dist), tags, hashes) .boxed() .await? } @@ -176,18 +186,21 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &cache_shard, subdirectory.as_deref(), tags, + hashes, ) .boxed() .await? } BuildableSource::Url(SourceUrl::Git(resource)) => { - self.git(source, resource, tags).boxed().await? + self.git(source, resource, tags, hashes).boxed().await? } BuildableSource::Url(SourceUrl::Path(resource)) => { if resource.path.is_dir() { - self.source_tree(source, resource, tags).boxed().await? + self.source_tree(source, resource, tags, hashes) + .boxed() + .await? } else { - self.archive(source, resource, tags).boxed().await? + self.archive(source, resource, tags, hashes).boxed().await? } } }; @@ -198,9 +211,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { /// Download a [`SourceDist`] and determine its metadata. This typically involves building the /// source distribution into a wheel; however, some build backends support determining the /// metadata without building the source distribution. - pub async fn download_and_build_metadata( + pub(super) async fn download_and_build_metadata( &self, source: &BuildableSource<'_>, + hashes: &[HashDigest], ) -> Result { let metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { @@ -220,6 +234,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { url: &url, path: Cow::Borrowed(path), }, + hashes, ) .boxed() .await; @@ -234,9 +249,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .join(dist.filename.version.to_string()), ); - self.url_metadata(source, &dist.file.filename, &url, &cache_shard, None) - .boxed() - .await? + self.url_metadata( + source, + &dist.file.filename, + &url, + &cache_shard, + None, + hashes, + ) + .boxed() + .await? } BuildableSource::Dist(SourceDist::DirectUrl(dist)) => { let filename = dist.filename().expect("Distribution must have a filename"); @@ -254,22 +276,23 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &url, &cache_shard, subdirectory.as_deref(), + hashes, ) .boxed() .await? } BuildableSource::Dist(SourceDist::Git(dist)) => { - self.git_metadata(source, &GitSourceUrl::from(dist)) + self.git_metadata(source, &GitSourceUrl::from(dist), hashes) .boxed() .await? } BuildableSource::Dist(SourceDist::Path(dist)) => { if dist.path.is_dir() { - self.source_tree_metadata(source, &PathSourceUrl::from(dist)) + self.source_tree_metadata(source, &PathSourceUrl::from(dist), hashes) .boxed() .await? } else { - self.archive_metadata(source, &PathSourceUrl::from(dist)) + self.archive_metadata(source, &PathSourceUrl::from(dist), hashes) .boxed() .await? } @@ -293,18 +316,23 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &url, &cache_shard, subdirectory.as_deref(), + hashes, ) .boxed() .await? } BuildableSource::Url(SourceUrl::Git(resource)) => { - self.git_metadata(source, resource).boxed().await? + self.git_metadata(source, resource, hashes).boxed().await? } BuildableSource::Url(SourceUrl::Path(resource)) => { if resource.path.is_dir() { - self.source_tree_metadata(source, resource).boxed().await? + self.source_tree_metadata(source, resource, hashes) + .boxed() + .await? } else { - self.archive_metadata(source, resource).boxed().await? + self.archive_metadata(source, resource, hashes) + .boxed() + .await? } } }; @@ -322,19 +350,29 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { cache_shard: &CacheShard, subdirectory: Option<&'data Path>, tags: &Tags, + hashes: &[HashDigest], ) -> Result { // Fetch the revision for the source distribution. let revision = self - .url_revision(source, filename, url, cache_shard) + .url_revision(source, filename, url, cache_shard, hashes) .await?; + // Before running the build, check that the hashes match. + if !revision.satisfies(hashes) { + return Err(Error::hash_mismatch( + source.to_string(), + hashes, + revision.hashes(), + )); + } + // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); // If the cache contains a compatible wheel, return it. if let Some(built_wheel) = BuiltWheelMetadata::find_in_cache(tags, &cache_shard) { - return Ok(built_wheel); + return Ok(built_wheel.with_hashes(revision.into_hashes())); } let task = self @@ -364,6 +402,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { path: cache_shard.join(&disk_filename), target: cache_shard.join(wheel_filename.stem()), filename: wheel_filename, + hashes: revision.into_hashes(), }) } @@ -379,12 +418,22 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { url: &'data Url, cache_shard: &CacheShard, subdirectory: Option<&'data Path>, + hashes: &[HashDigest], ) -> Result { // Fetch the revision for the source distribution. let revision = self - .url_revision(source, filename, url, cache_shard) + .url_revision(source, filename, url, cache_shard, hashes) .await?; + // Before running the build, check that the hashes match. + if !revision.satisfies(hashes) { + return Err(Error::hash_mismatch( + source.to_string(), + hashes, + revision.hashes(), + )); + } + // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); @@ -449,6 +498,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { filename: &str, url: &Url, cache_shard: &CacheShard, + hashes: &[HashDigest], ) -> Result { let cache_entry = cache_shard.entry(REVISION); let cache_control = match self.client.connectivity() { @@ -469,24 +519,40 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Download the source distribution. debug!("Downloading source distribution: {source}"); - let source_dist_entry = cache_shard.shard(revision.id()).entry(filename); - self.persist_url(response, source, filename, &source_dist_entry) + let entry = cache_shard.shard(revision.id()).entry(filename); + let hashes = self + .download_archive(response, source, filename, entry.path(), hashes) .await?; - Ok(revision) + Ok(revision.with_hashes(hashes)) } .boxed() .instrument(info_span!("download", source_dist = %source)) }; let req = self.request(url.clone())?; - self.client + let revision = self + .client .cached_client() .get_serde(req, &cache_entry, cache_control, download) .await .map_err(|err| match err { CachedClientError::Callback(err) => err, CachedClientError::Client(err) => Error::Client(err), - }) + })?; + + // If the archive is missing the required hashes, force a refresh. + if revision.has_digests(hashes) { + Ok(revision) + } else { + self.client + .cached_client() + .skip_cache(self.request(url.clone())?, &cache_entry, download) + .await + .map_err(|err| match err { + CachedClientError::Callback(err) => err, + CachedClientError::Client(err) => Error::Client(err), + }) + } } /// Build a source distribution from a local archive (e.g., `.tar.gz` or `.zip`). @@ -495,6 +561,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, tags: &Tags, + hashes: &[HashDigest], ) -> Result { let cache_shard = self.build_context.cache().shard( CacheBucket::BuiltWheels, @@ -503,9 +570,18 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Fetch the revision for the source distribution. let revision = self - .archive_revision(source, resource, &cache_shard) + .archive_revision(source, resource, &cache_shard, hashes) .await?; + // Before running the build, check that the hashes match. + if !revision.satisfies(hashes) { + return Err(Error::hash_mismatch( + source.to_string(), + hashes, + revision.hashes(), + )); + } + // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); @@ -543,6 +619,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { path: cache_shard.join(&disk_filename), target: cache_shard.join(filename.stem()), filename, + hashes: revision.into_hashes(), }) } @@ -554,6 +631,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, + hashes: &[HashDigest], ) -> Result { let cache_shard = self.build_context.cache().shard( CacheBucket::BuiltWheels, @@ -562,9 +640,18 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Fetch the revision for the source distribution. let revision = self - .archive_revision(source, resource, &cache_shard) + .archive_revision(source, resource, &cache_shard, hashes) .await?; + // Before running the build, check that the hashes match. + if !revision.satisfies(hashes) { + return Err(Error::hash_mismatch( + source.to_string(), + hashes, + revision.hashes(), + )); + } + // Scope all operations to the revision. Within the revision, there's no need to check for // freshness, since entries have to be fresher than the revision itself. let cache_shard = cache_shard.shard(revision.id()); @@ -627,6 +714,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, cache_shard: &CacheShard, + hashes: &[HashDigest], ) -> Result { // Determine the last-modified time of the source distribution. let modified = ArchiveTimestamp::from_file(&resource.path).map_err(Error::CacheRead)?; @@ -637,7 +725,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // If the revision already exists, return it. There's no need to check for freshness, since // we use an exact timestamp. if let Some(revision) = read_timestamped_revision(&revision_entry, modified)? { - return Ok(revision); + if revision.has_digests(hashes) { + return Ok(revision); + } } // Otherwise, we need to create a new revision. @@ -646,7 +736,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // Unzip the archive to a temporary directory. debug!("Unpacking source distribution: {source}"); let entry = cache_shard.shard(revision.id()).entry("source"); - self.persist_archive(&resource.path, source, &entry).await?; + let hashes = self + .persist_archive(&resource.path, entry.path(), hashes) + .await?; + let revision = revision.with_hashes(hashes); // Persist the revision. write_atomic( @@ -668,7 +761,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, tags: &Tags, + hashes: &[HashDigest], ) -> Result { + // Before running the build, check that the hashes match. + if !hashes.is_empty() { + return Err(Error::HashesNotSupportedSourceTree(source.to_string())); + } + let cache_shard = self.build_context.cache().shard( CacheBucket::BuiltWheels, WheelCache::Path(resource.url).root(), @@ -714,6 +813,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { path: cache_shard.join(&disk_filename), target: cache_shard.join(filename.stem()), filename, + hashes: vec![], }) } @@ -725,7 +825,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, + hashes: &[HashDigest], ) -> Result { + // Before running the build, check that the hashes match. + if !hashes.is_empty() { + return Err(Error::HashesNotSupportedSourceTree(source.to_string())); + } + let cache_shard = self.build_context.cache().shard( CacheBucket::BuiltWheels, WheelCache::Path(resource.url).root(), @@ -742,16 +848,9 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { // If the cache contains compatible metadata, return it. let metadata_entry = cache_shard.entry(METADATA); - if self - .build_context - .cache() - .freshness(&metadata_entry, source.name()) - .is_ok_and(Freshness::is_fresh) - { - if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { - debug!("Using cached metadata for: {source}"); - return Ok(metadata); - } + if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { + debug!("Using cached metadata for: {source}"); + return Ok(metadata); } // If the backend supports `prepare_metadata_for_build_wheel`, use it. @@ -828,7 +927,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &GitSourceUrl<'_>, tags: &Tags, + hashes: &[HashDigest], ) -> Result { + // Before running the build, check that the hashes match. + if !hashes.is_empty() { + return Err(Error::HashesNotSupportedGit(source.to_string())); + } + // Resolve to a precise Git SHA. let url = if let Some(url) = resolve_precise( resource.url, @@ -882,6 +987,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { path: cache_shard.join(&disk_filename), target: cache_shard.join(filename.stem()), filename, + hashes: vec![], }) } @@ -893,7 +999,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, resource: &GitSourceUrl<'_>, + hashes: &[HashDigest], ) -> Result { + // Before running the build, check that the hashes match. + if !hashes.is_empty() { + return Err(Error::HashesNotSupportedGit(source.to_string())); + } + // Resolve to a precise Git SHA. let url = if let Some(url) = resolve_precise( resource.url, @@ -975,21 +1087,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } /// Download and unzip a source distribution into the cache from an HTTP response. - async fn persist_url( + async fn download_archive( &self, response: Response, source: &BuildableSource<'_>, filename: &str, - cache_entry: &CacheEntry, - ) -> Result<(), Error> { - let cache_path = cache_entry.path(); - if cache_path.is_dir() { - debug!("Distribution is already cached: {source}"); - return Ok(()); - } - - // Download and unzip the source distribution into a temporary directory. - let span = info_span!("persist_url", filename = filename, source_dist = %source); + target: &Path, + hashes: &[HashDigest], + ) -> Result, Error> { let temp_dir = tempfile::tempdir_in(self.build_context.cache().bucket(CacheBucket::BuiltWheels)) .map_err(Error::CacheWrite)?; @@ -997,9 +1102,29 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .bytes_stream() .map_err(|err| std::io::Error::new(std::io::ErrorKind::Other, err)) .into_async_read(); - uv_extract::stream::archive(reader.compat(), filename, temp_dir.path()).await?; + + // Create a hasher for each hash algorithm. + let algorithms = { + let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); + hash.sort(); + hash.dedup(); + hash + }; + let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); + let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers); + + // Download and unzip the source distribution into a temporary directory. + let span = info_span!("download_source_dist", filename = filename, source_dist = %source); + uv_extract::stream::archive(&mut hasher, filename, temp_dir.path()).await?; drop(span); + // If necessary, exhaust the reader to compute the hash. + if !hashes.is_empty() { + hasher.finish().await.map_err(Error::HashExhaustion)?; + } + + let hashes = hashers.into_iter().map(HashDigest::from).collect(); + // Extract the top-level directory. let extracted = match uv_extract::strip_component(temp_dir.path()) { Ok(top_level) => top_level, @@ -1008,39 +1133,51 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // Persist it to the cache. - fs_err::tokio::create_dir_all(cache_path.parent().expect("Cache entry to have parent")) + fs_err::tokio::create_dir_all(target.parent().expect("Cache entry to have parent")) .await .map_err(Error::CacheWrite)?; - fs_err::tokio::rename(extracted, &cache_path) + fs_err::tokio::rename(extracted, target) .await .map_err(Error::CacheWrite)?; - Ok(()) + Ok(hashes) } /// Extract a local archive, and store it at the given [`CacheEntry`]. async fn persist_archive( &self, path: &Path, - source: &BuildableSource<'_>, - cache_entry: &CacheEntry, - ) -> Result<(), Error> { - let cache_path = cache_entry.path(); - if cache_path.is_dir() { - debug!("Distribution is already cached: {source}"); - return Ok(()); - } - + target: &Path, + hashes: &[HashDigest], + ) -> Result, Error> { debug!("Unpacking for build: {}", path.display()); - // Unzip the archive into a temporary directory. let temp_dir = tempfile::tempdir_in(self.build_context.cache().bucket(CacheBucket::BuiltWheels)) .map_err(Error::CacheWrite)?; let reader = fs_err::tokio::File::open(&path) .await .map_err(Error::CacheRead)?; - uv_extract::seek::archive(reader, path, &temp_dir.path()).await?; + + // Create a hasher for each hash algorithm. + let algorithms = { + let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); + hash.sort(); + hash.dedup(); + hash + }; + let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); + let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers); + + // Unzip the archive into a temporary directory. + uv_extract::stream::archive(&mut hasher, path, &temp_dir.path()).await?; + + // If necessary, exhaust the reader to compute the hash. + if !hashes.is_empty() { + hasher.finish().await.map_err(Error::HashExhaustion)?; + } + + let hashes = hashers.into_iter().map(HashDigest::from).collect(); // Extract the top-level directory from the archive. let extracted = match uv_extract::strip_component(temp_dir.path()) { @@ -1050,14 +1187,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // Persist it to the cache. - fs_err::tokio::create_dir_all(cache_path.parent().expect("Cache entry to have parent")) + fs_err::tokio::create_dir_all(target.parent().expect("Cache entry to have parent")) .await .map_err(Error::CacheWrite)?; - fs_err::tokio::rename(extracted, &cache_path) + fs_err::tokio::rename(extracted, &target) .await .map_err(Error::CacheWrite)?; - Ok(()) + Ok(hashes) } /// Build a source distribution, storing the built wheel in the cache. diff --git a/crates/uv-distribution/src/source/revision.rs b/crates/uv-distribution/src/source/revision.rs index b2f6d5b9a..aadc2945a 100644 --- a/crates/uv-distribution/src/source/revision.rs +++ b/crates/uv-distribution/src/source/revision.rs @@ -1,5 +1,8 @@ +use distribution_types::Hashed; use serde::{Deserialize, Serialize}; +use pypi_types::HashDigest; + /// The [`Revision`] is a thin wrapper around a unique identifier for the source distribution. /// /// A revision represents a unique version of a source distribution, at a level more granular than @@ -7,16 +10,45 @@ use serde::{Deserialize, Serialize}; /// at a URL or a local file path may have multiple revisions, each representing a unique state of /// the distribution, despite the reported version number remaining the same. #[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct Revision(String); +pub(crate) struct Revision { + id: String, + hashes: Vec, +} impl Revision { /// Initialize a new [`Revision`] with a random UUID. pub(crate) fn new() -> Self { - Self(nanoid::nanoid!()) + Self { + id: nanoid::nanoid!(), + hashes: vec![], + } } - /// Return the unique ID of the revision. + /// Return the unique ID of the manifest. pub(crate) fn id(&self) -> &str { - &self.0 + &self.id + } + + /// Return the computed hashes of the archive. + pub(crate) fn hashes(&self) -> &[HashDigest] { + &self.hashes + } + + /// Return the computed hashes of the archive. + pub(crate) fn into_hashes(self) -> Vec { + self.hashes + } + + /// Set the computed hashes of the archive. + #[must_use] + pub(crate) fn with_hashes(mut self, hashes: Vec) -> Self { + self.hashes = hashes; + self + } +} + +impl Hashed for Revision { + fn hashes(&self) -> &[HashDigest] { + &self.hashes } } diff --git a/crates/uv-extract/Cargo.toml b/crates/uv-extract/Cargo.toml index f40dee266..0c3dbb8fb 100644 --- a/crates/uv-extract/Cargo.toml +++ b/crates/uv-extract/Cargo.toml @@ -13,12 +13,16 @@ license = { workspace = true } workspace = true [dependencies] +pypi-types = { workspace = true } + async-compression = { workspace = true, features = ["gzip", "zstd"] } async_zip = { workspace = true, features = ["tokio"] } fs-err = { workspace = true, features = ["tokio"] } futures = { workspace = true } +md-5.workspace = true rayon = { workspace = true } rustc-hash = { workspace = true } +sha2 = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["io-util"] } tokio-tar = { workspace = true } diff --git a/crates/uv-extract/src/hash.rs b/crates/uv-extract/src/hash.rs new file mode 100644 index 000000000..22072f7c8 --- /dev/null +++ b/crates/uv-extract/src/hash.rs @@ -0,0 +1,146 @@ +use std::pin::Pin; +use std::task::{Context, Poll}; + +use sha2::Digest; +use tokio::io::{AsyncReadExt, ReadBuf}; + +use pypi_types::{HashAlgorithm, HashDigest}; + +pub struct Sha256Reader<'a, R> { + reader: R, + hasher: &'a mut sha2::Sha256, +} + +impl<'a, R> Sha256Reader<'a, R> +where + R: tokio::io::AsyncRead + Unpin, +{ + pub fn new(reader: R, hasher: &'a mut sha2::Sha256) -> Self { + Sha256Reader { reader, hasher } + } +} + +impl<'a, R> tokio::io::AsyncRead for Sha256Reader<'a, R> +where + R: tokio::io::AsyncRead + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let reader = Pin::new(&mut self.reader); + match reader.poll_read(cx, buf) { + Poll::Ready(Ok(())) => { + self.hasher.update(buf.filled()); + Poll::Ready(Ok(())) + } + other => other, + } + } +} + +#[derive(Debug)] +pub enum Hasher { + Md5(md5::Md5), + Sha256(sha2::Sha256), + Sha384(sha2::Sha384), + Sha512(sha2::Sha512), +} + +impl Hasher { + pub fn update(&mut self, data: &[u8]) { + match self { + Hasher::Md5(hasher) => hasher.update(data), + Hasher::Sha256(hasher) => hasher.update(data), + Hasher::Sha384(hasher) => hasher.update(data), + Hasher::Sha512(hasher) => hasher.update(data), + } + } + + pub fn finalize(self) -> Vec { + match self { + Hasher::Md5(hasher) => hasher.finalize().to_vec(), + Hasher::Sha256(hasher) => hasher.finalize().to_vec(), + Hasher::Sha384(hasher) => hasher.finalize().to_vec(), + Hasher::Sha512(hasher) => hasher.finalize().to_vec(), + } + } +} + +impl From for Hasher { + fn from(algorithm: HashAlgorithm) -> Self { + match algorithm { + HashAlgorithm::Md5 => Hasher::Md5(md5::Md5::new()), + HashAlgorithm::Sha256 => Hasher::Sha256(sha2::Sha256::new()), + HashAlgorithm::Sha384 => Hasher::Sha384(sha2::Sha384::new()), + HashAlgorithm::Sha512 => Hasher::Sha512(sha2::Sha512::new()), + } + } +} + +impl From for HashDigest { + fn from(hasher: Hasher) -> Self { + match hasher { + Hasher::Md5(hasher) => HashDigest { + algorithm: HashAlgorithm::Md5, + digest: format!("{:x}", hasher.finalize()).into_boxed_str(), + }, + Hasher::Sha256(hasher) => HashDigest { + algorithm: HashAlgorithm::Sha256, + digest: format!("{:x}", hasher.finalize()).into_boxed_str(), + }, + Hasher::Sha384(hasher) => HashDigest { + algorithm: HashAlgorithm::Sha384, + digest: format!("{:x}", hasher.finalize()).into_boxed_str(), + }, + Hasher::Sha512(hasher) => HashDigest { + algorithm: HashAlgorithm::Sha512, + digest: format!("{:x}", hasher.finalize()).into_boxed_str(), + }, + } + } +} + +pub struct HashReader<'a, R> { + reader: R, + hashers: &'a mut [Hasher], +} + +impl<'a, R> HashReader<'a, R> +where + R: tokio::io::AsyncRead + Unpin, +{ + pub fn new(reader: R, hashers: &'a mut [Hasher]) -> Self { + HashReader { reader, hashers } + } + + /// Exhaust the underlying reader. + pub async fn finish(&mut self) -> Result<(), std::io::Error> { + while self.read(&mut vec![0; 8192]).await? > 0 {} + + Ok(()) + } +} + +impl<'a, R> tokio::io::AsyncRead for HashReader<'a, R> +where + R: tokio::io::AsyncRead + Unpin, +{ + fn poll_read( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + let reader = Pin::new(&mut self.reader); + match reader.poll_read(cx, buf) { + Poll::Ready(Ok(())) => { + for hasher in self.hashers.iter_mut() { + hasher.update(buf.filled()); + } + Poll::Ready(Ok(())) + } + other => other, + } + } +} diff --git a/crates/uv-extract/src/lib.rs b/crates/uv-extract/src/lib.rs index 20d433071..192aaa8e0 100644 --- a/crates/uv-extract/src/lib.rs +++ b/crates/uv-extract/src/lib.rs @@ -2,6 +2,7 @@ pub use error::Error; pub use sync::*; mod error; +pub mod hash; pub mod seek; pub mod stream; mod sync; diff --git a/crates/uv-extract/src/stream.rs b/crates/uv-extract/src/stream.rs index f9ac12148..e73db2ae4 100644 --- a/crates/uv-extract/src/stream.rs +++ b/crates/uv-extract/src/stream.rs @@ -161,7 +161,8 @@ pub async fn untar_gz( let mut archive = tokio_tar::ArchiveBuilder::new(decompressed_bytes) .set_preserve_mtime(false) .build(); - Ok(untar_in(&mut archive, target.as_ref()).await?) + untar_in(&mut archive, target.as_ref()).await?; + Ok(()) } /// Unzip a `.tar.zst` archive into the target directory, without requiring `Seek`. diff --git a/crates/uv-installer/Cargo.toml b/crates/uv-installer/Cargo.toml index 413664f62..789fba48d 100644 --- a/crates/uv-installer/Cargo.toml +++ b/crates/uv-installer/Cargo.toml @@ -22,6 +22,7 @@ pypi-types = { workspace = true } requirements-txt = { workspace = true } uv-cache = { workspace = true } uv-client = { workspace = true } +uv-configuration = { workspace = true } uv-distribution = { workspace = true } uv-extract = { workspace = true } uv-fs = { workspace = true } @@ -29,13 +30,13 @@ uv-interpreter = { workspace = true } uv-normalize = { workspace = true } uv-types = { workspace = true } uv-warnings = { workspace = true } -uv-configuration = { workspace = true } anyhow = { workspace = true } async-channel = { workspace = true } fs-err = { workspace = true } futures = { workspace = true } rayon = { workspace = true } +rmp-serde = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true } tempfile = { workspace = true } diff --git a/crates/uv-installer/src/downloader.rs b/crates/uv-installer/src/downloader.rs index bba3c2fa7..1c60f46e5 100644 --- a/crates/uv-installer/src/downloader.rs +++ b/crates/uv-installer/src/downloader.rs @@ -8,13 +8,14 @@ use tracing::instrument; use url::Url; use distribution_types::{ - BuildableSource, CachedDist, Dist, Identifier, LocalEditable, LocalEditables, RemoteSource, + BuildableSource, CachedDist, Dist, Hashed, Identifier, LocalEditable, LocalEditables, Name, + RemoteSource, }; use platform_tags::Tags; use uv_cache::Cache; use uv_client::RegistryClient; -use uv_distribution::DistributionDatabase; -use uv_types::{BuildContext, InFlight}; +use uv_distribution::{DistributionDatabase, LocalWheel}; +use uv_types::{BuildContext, InFlight, RequiredHashes}; use crate::editable::BuiltEditable; @@ -39,6 +40,7 @@ pub enum Error { pub struct Downloader<'a, Context: BuildContext + Send + Sync> { tags: &'a Tags, cache: &'a Cache, + hashes: &'a RequiredHashes, database: DistributionDatabase<'a, Context>, reporter: Option>, } @@ -47,12 +49,14 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { pub fn new( cache: &'a Cache, tags: &'a Tags, + hashes: &'a RequiredHashes, client: &'a RegistryClient, build_context: &'a Context, ) -> Self { Self { tags, cache, + hashes, database: DistributionDatabase::new(client, build_context), reporter: None, } @@ -65,6 +69,7 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { Self { tags: self.tags, cache: self.cache, + hashes: self.hashes, database: self.database.with_reporter(Facade::from(reporter.clone())), reporter: Some(reporter.clone()), } @@ -165,12 +170,27 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { pub async fn get_wheel(&self, dist: Dist, in_flight: &InFlight) -> Result { let id = dist.distribution_id(); if in_flight.downloads.register(id.clone()) { + let hashes = self.hashes.get(dist.name()).unwrap_or_default(); let result = self .database - .get_or_build_wheel(&dist, self.tags) + .get_or_build_wheel(&dist, self.tags, hashes) .boxed() .map_err(|err| Error::Fetch(dist.clone(), err)) .await + .and_then(|wheel: LocalWheel| { + if wheel.satisfies(hashes) { + Ok(wheel) + } else { + Err(Error::Fetch( + dist.clone(), + uv_distribution::Error::hash_mismatch( + dist.to_string(), + hashes, + wheel.hashes(), + ), + )) + } + }) .map(CachedDist::from); match result { Ok(cached) => { diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 25f74a48d..2151194ae 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -6,6 +6,7 @@ use anyhow::{bail, Result}; use rustc_hash::FxHashMap; use tracing::{debug, warn}; +use distribution_types::Hashed; use distribution_types::{ BuiltDist, CachedDirectUrlDist, CachedDist, Dist, IndexLocations, InstalledDist, InstalledMetadata, InstalledVersion, Name, SourceDist, @@ -13,10 +14,12 @@ use distribution_types::{ use pep508_rs::{Requirement, VersionOrUrl}; use platform_tags::Tags; use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache, CacheBucket, WheelCache}; +use uv_client::DataWithCachePolicy; use uv_configuration::{NoBinary, Reinstall}; -use uv_distribution::{BuiltWheelIndex, RegistryWheelIndex}; +use uv_distribution::{read_timestamped_archive, Archive, BuiltWheelIndex, RegistryWheelIndex}; use uv_fs::Simplified; use uv_interpreter::PythonEnvironment; +use uv_types::RequiredHashes; use crate::{ResolvedEditable, SitePackages}; @@ -53,20 +56,25 @@ impl<'a> Planner<'a> { /// plan will respect cache entries created after the current time (as per the [`Refresh`] /// policy). Otherwise, entries will be ignored. The downstream distribution database may still /// read those entries from the cache after revalidating them. + /// + /// The install plan will also respect the required hashes, such that it will never return a + /// cached distribution that does not match the required hash. Like pip, though, it _will_ + /// return an _installed_ distribution that does not match the required hash. #[allow(clippy::too_many_arguments)] pub fn build( self, mut site_packages: SitePackages<'_>, reinstall: &Reinstall, no_binary: &NoBinary, + hashes: &RequiredHashes, index_locations: &IndexLocations, cache: &Cache, venv: &PythonEnvironment, tags: &Tags, ) -> Result { // Index all the already-downloaded wheels in the cache. - let mut registry_index = RegistryWheelIndex::new(cache, tags, index_locations); - let built_index = BuiltWheelIndex::new(cache, tags); + let mut registry_index = RegistryWheelIndex::new(cache, tags, index_locations, hashes); + let built_index = BuiltWheelIndex::new(cache, tags, hashes); let mut cached = vec![]; let mut remote = vec![]; @@ -206,16 +214,9 @@ impl<'a> Planner<'a> { } } Some(VersionOrUrl::VersionSpecifier(specifier)) => { - if let Some(distribution) = - registry_index - .get(&requirement.name) - .find_map(|(version, distribution)| { - if specifier.contains(version) { - Some(distribution) - } else { - None - } - }) + if let Some((_version, distribution)) = registry_index + .get(&requirement.name) + .find(|(version, _)| specifier.contains(version)) { debug!("Requirement already cached: {distribution}"); cached.push(CachedDist::Registry(distribution.clone())); @@ -252,19 +253,30 @@ impl<'a> Planner<'a> { CacheBucket::Wheels, WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()), ) - .entry(wheel.filename.stem()); + .entry(format!("{}.http", wheel.filename.stem())); - match cache_entry.path().canonicalize() { - Ok(archive) => { - let cached_dist = CachedDirectUrlDist::from_url( - wheel.filename, - wheel.url, - archive, - ); + // Read the HTTP pointer. + match fs_err::File::open(cache_entry.path()) { + Ok(file) => { + let data = DataWithCachePolicy::from_reader(file)?.data; + let archive = rmp_serde::from_slice::(&data)?; - debug!("URL wheel requirement already cached: {cached_dist}"); - cached.push(CachedDist::Url(cached_dist)); - continue; + // Enforce hash checking. + let hashes = hashes.get(&requirement.name).unwrap_or_default(); + if archive.satisfies(hashes) { + let cached_dist = CachedDirectUrlDist::from_url( + wheel.filename, + wheel.url, + archive.hashes, + archive.path, + ); + + debug!( + "URL wheel requirement already cached: {cached_dist}" + ); + cached.push(CachedDist::Url(cached_dist)); + continue; + } } Err(err) if err.kind() == io::ErrorKind::NotFound => { // The cache entry doesn't exist, so it's not fresh. @@ -294,31 +306,25 @@ impl<'a> Planner<'a> { CacheBucket::Wheels, WheelCache::Url(&wheel.url).wheel_dir(wheel.name().as_ref()), ) - .entry(wheel.filename.stem()); + .entry(format!("{}.rev", wheel.filename.stem())); - match cache_entry.path().canonicalize() { - Ok(archive) => { - if ArchiveTimestamp::up_to_date_with( - &wheel.path, - ArchiveTarget::Cache(&archive), - )? { - let cached_dist = CachedDirectUrlDist::from_url( - wheel.filename, - wheel.url, - archive, - ); + if let Some(archive) = read_timestamped_archive( + &cache_entry, + ArchiveTimestamp::from_file(&wheel.path)?, + )? { + let hashes = hashes.get(&requirement.name).unwrap_or_default(); + if archive.satisfies(hashes) { + let cached_dist = CachedDirectUrlDist::from_url( + wheel.filename, + wheel.url, + archive.hashes, + archive.path, + ); - debug!( - "URL wheel requirement already cached: {cached_dist}" - ); - cached.push(CachedDist::Url(cached_dist)); - continue; - } + debug!("Path wheel requirement already cached: {cached_dist}"); + cached.push(CachedDist::Url(cached_dist)); + continue; } - Err(err) if err.kind() == io::ErrorKind::NotFound => { - // The cache entry doesn't exist, so it's not fresh. - } - Err(err) => return Err(err.into()), } } Dist::Source(SourceDist::DirectUrl(sdist)) => { diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index d2c0131fc..e38521bac 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -1,19 +1,18 @@ use std::collections::VecDeque; use anyhow::{Context, Result}; - use futures::stream::FuturesUnordered; use futures::StreamExt; use rustc_hash::FxHashSet; -use distribution_types::{Dist, DistributionMetadata, LocalEditable}; +use distribution_types::{Dist, DistributionMetadata, LocalEditable, Name}; use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use pypi_types::Metadata23; use uv_client::RegistryClient; use uv_configuration::{Constraints, Overrides}; use uv_distribution::{DistributionDatabase, Reporter}; use uv_resolver::{InMemoryIndex, MetadataResponse}; -use uv_types::{BuildContext, RequestedRequirements}; +use uv_types::{BuildContext, RequestedRequirements, RequiredHashes}; /// A resolver for resolving lookahead requirements from direct URLs. /// @@ -40,6 +39,8 @@ pub struct LookaheadResolver<'a, Context: BuildContext + Send + Sync> { overrides: &'a Overrides, /// The editable requirements for the project. editables: &'a [(LocalEditable, Metadata23)], + /// The required hashes for the project. + hashes: &'a RequiredHashes, /// The in-memory index for resolving dependencies. index: &'a InMemoryIndex, /// The database for fetching and building distributions. @@ -48,11 +49,13 @@ pub struct LookaheadResolver<'a, Context: BuildContext + Send + Sync> { impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { /// Instantiate a new [`LookaheadResolver`] for a given set of requirements. + #[allow(clippy::too_many_arguments)] pub fn new( requirements: &'a [Requirement], constraints: &'a Constraints, overrides: &'a Overrides, editables: &'a [(LocalEditable, Metadata23)], + hashes: &'a RequiredHashes, context: &'a Context, client: &'a RegistryClient, index: &'a InMemoryIndex, @@ -62,6 +65,7 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { constraints, overrides, editables, + hashes, index, database: DistributionDatabase::new(client, context), } @@ -151,9 +155,10 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { metadata.requires_dist.clone() } else { // Run the PEP 517 build process to extract metadata from the source distribution. + let hashes = self.hashes.get(dist.name()).unwrap_or_default(); let metadata = self .database - .get_or_build_wheel_metadata(&dist) + .get_or_build_wheel_metadata(&dist, hashes) .await .with_context(|| match &dist { Dist::Built(built) => format!("Failed to download: {built}"), diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index a734db17b..b6cccd01b 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -1,5 +1,4 @@ use std::borrow::Cow; - use std::path::{Path, PathBuf}; use anyhow::{Context, Result}; @@ -25,6 +24,8 @@ pub struct SourceTreeResolver<'a, Context: BuildContext + Send + Sync> { source_trees: Vec, /// The extras to include when resolving requirements. extras: &'a ExtrasSpecification<'a>, + /// Whether to require hashes for all dependencies. + require_hashes: bool, /// The in-memory index for resolving dependencies. index: &'a InMemoryIndex, /// The database for fetching and building distributions. @@ -36,6 +37,7 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { pub fn new( source_trees: Vec, extras: &'a ExtrasSpecification<'a>, + require_hashes: bool, context: &'a Context, client: &'a RegistryClient, index: &'a InMemoryIndex, @@ -43,6 +45,7 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { Self { source_trees, extras, + require_hashes, index, database: DistributionDatabase::new(client, context), } @@ -84,6 +87,16 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { path: Cow::Owned(path), }); + // TODO(charlie): Should we enforce this earlier? If the metadata can be extracted + // statically, it won't go through this resolver. But we'll fail anyway, since the + // dependencies (when extracted from a `pyproject.toml` or `setup.py`) won't include hashes. + if self.require_hashes { + return Err(anyhow::anyhow!( + "Hash-checking is not supported for local directories: {}", + source_tree.user_display() + )); + } + // Fetch the metadata for the distribution. let metadata = { let id = PackageId::from_url(source.url()); @@ -104,7 +117,7 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { } else { // Run the PEP 517 build process to extract metadata from the source distribution. let source = BuildableSource::Url(source); - let metadata = self.database.build_wheel_metadata(&source).await?; + let metadata = self.database.build_wheel_metadata(&source, &[]).await?; // Insert the metadata into the index. self.index diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index a952e5473..9feb8514d 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -7,7 +7,7 @@ use tracing::{instrument, Level}; use cache_key::CanonicalUrl; use distribution_types::{FlatIndexLocation, IndexUrl}; use pep508_rs::{Requirement, RequirementsTxtRequirement}; -use requirements_txt::{EditableRequirement, FindLink, RequirementsTxt}; +use requirements_txt::{EditableRequirement, FindLink, RequirementEntry, RequirementsTxt}; use uv_client::BaseClientBuilder; use uv_configuration::{NoBinary, NoBuild}; use uv_fs::Simplified; @@ -20,6 +20,8 @@ use crate::{ExtrasSpecification, RequirementsSource}; pub struct RequirementsSpecification { /// The name of the project specifying requirements. pub project: Option, + /// The `requirements.txt` entries for the project. + pub entries: Vec, /// The requirements for the project. pub requirements: Vec, /// The constraints for the project. @@ -60,6 +62,7 @@ impl RequirementsSpecification { .with_context(|| format!("Failed to parse `{name}`"))?; Self { project: None, + entries: vec![], requirements: vec![requirement], constraints: vec![], overrides: vec![], @@ -79,6 +82,7 @@ impl RequirementsSpecification { .with_context(|| format!("Failed to parse `{name}`"))?; Self { project: None, + entries: vec![], requirements: vec![], constraints: vec![], overrides: vec![], @@ -98,6 +102,7 @@ impl RequirementsSpecification { RequirementsTxt::parse(path, std::env::current_dir()?, client_builder).await?; Self { project: None, + entries: requirements_txt.requirements.clone(), requirements: requirements_txt .requirements .into_iter() @@ -148,6 +153,7 @@ impl RequirementsSpecification { { Self { project: Some(project.name), + entries: vec![], requirements: project .requirements .into_iter() @@ -175,6 +181,7 @@ impl RequirementsSpecification { })?; Self { project: None, + entries: vec![], requirements: vec![], constraints: vec![], overrides: vec![], @@ -200,6 +207,7 @@ impl RequirementsSpecification { })?; Self { project: None, + entries: vec![], requirements: vec![], constraints: vec![], overrides: vec![], @@ -232,6 +240,7 @@ impl RequirementsSpecification { // a requirements file can also add constraints. for source in requirements { let source = Self::from_source(source, extras, client_builder).await?; + spec.entries.extend(source.entries); spec.requirements.extend(source.requirements); spec.constraints.extend(source.constraints); spec.overrides.extend(source.overrides); @@ -261,7 +270,8 @@ impl RequirementsSpecification { spec.no_build.extend(source.no_build); } - // Read all constraints, treating _everything_ as a constraint. + // Read all constraints, treating _everything_ as a constraint. The raw entries (i.e., + // hashes) are ignored, as they are not relevant for constraints. for source in constraints { let source = Self::from_source(source, extras, client_builder).await?; for requirement in source.requirements { @@ -311,6 +321,7 @@ impl RequirementsSpecification { } } } + spec.entries.extend(source.entries); spec.overrides.extend(source.constraints); spec.overrides.extend(source.overrides); diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index 96e5cc179..e658852ae 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -27,6 +27,8 @@ use uv_types::BuildContext; pub struct NamedRequirementsResolver<'a, Context: BuildContext + Send + Sync> { /// The requirements for the project. requirements: Vec, + /// Whether to check hashes for distributions. + require_hashes: bool, /// The in-memory index for resolving dependencies. index: &'a InMemoryIndex, /// The database for fetching and building distributions. @@ -37,12 +39,14 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont /// Instantiate a new [`NamedRequirementsResolver`] for a given set of requirements. pub fn new( requirements: Vec, + require_hashes: bool, context: &'a Context, client: &'a RegistryClient, index: &'a InMemoryIndex, ) -> Self { Self { requirements, + require_hashes, index, database: DistributionDatabase::new(client, context), } @@ -61,6 +65,7 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont pub async fn resolve(self) -> Result> { let Self { requirements, + require_hashes, index, database, } = self; @@ -69,7 +74,8 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont match requirement { RequirementsTxtRequirement::Pep508(requirement) => Ok(requirement), RequirementsTxtRequirement::Unnamed(requirement) => { - Self::resolve_requirement(requirement, index, &database).await + Self::resolve_requirement(requirement, require_hashes, index, &database) + .await } } }) @@ -81,6 +87,7 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont /// Infer the package name for a given "unnamed" requirement. async fn resolve_requirement( requirement: UnnamedRequirement, + require_hashes: bool, index: &InMemoryIndex, database: &DistributionDatabase<'a, Context>, ) -> Result { @@ -233,6 +240,13 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont } }; + // TODO(charlie): Support `--require-hashes` for unnamed requirements. + if require_hashes { + return Err(anyhow::anyhow!( + "Unnamed requirements are not supported with `--require-hashes`" + )); + } + // Fetch the metadata for the distribution. let name = { let id = PackageId::from_url(source.url()); @@ -248,7 +262,7 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont } else { // Run the PEP 517 build process to extract metadata from the source distribution. let source = BuildableSource::Url(source); - let metadata = database.build_wheel_metadata(&source).await?; + let metadata = database.build_wheel_metadata(&source, &[]).await?; let name = metadata.name.clone(); diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 623136dff..8391ad6aa 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -93,6 +93,9 @@ pub enum ResolveError { #[error("Attempted to construct an invalid version specifier")] InvalidVersion(#[from] pep440_rs::VersionSpecifierBuildError), + #[error("In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: {0}")] + UnhashedPackage(PackageName), + /// Something unexpected happened. #[error("{0}")] Failure(String), diff --git a/crates/uv-resolver/src/flat_index.rs b/crates/uv-resolver/src/flat_index.rs index 820ad310a..3e09689bc 100644 --- a/crates/uv-resolver/src/flat_index.rs +++ b/crates/uv-resolver/src/flat_index.rs @@ -11,9 +11,11 @@ use distribution_types::{ }; use pep440_rs::Version; use platform_tags::Tags; +use pypi_types::HashDigest; use uv_client::FlatIndexEntries; use uv_configuration::{NoBinary, NoBuild}; use uv_normalize::PackageName; +use uv_types::RequiredHashes; /// A set of [`PrioritizedDist`] from a `--find-links` entry, indexed by [`PackageName`] /// and [`Version`]. @@ -32,6 +34,7 @@ impl FlatIndex { pub fn from_entries( entries: FlatIndexEntries, tags: &Tags, + required_hashes: &RequiredHashes, no_build: &NoBuild, no_binary: &NoBinary, ) -> Self { @@ -44,6 +47,7 @@ impl FlatIndex { file, filename, tags, + required_hashes, no_build, no_binary, url, @@ -56,11 +60,13 @@ impl FlatIndex { Self { index, offline } } + #[allow(clippy::too_many_arguments)] fn add_file( distributions: &mut FlatDistributions, file: File, filename: DistFilename, tags: &Tags, + required_hashes: &RequiredHashes, no_build: &NoBuild, no_binary: &NoBinary, index: IndexUrl, @@ -71,7 +77,13 @@ impl FlatIndex { DistFilename::WheelFilename(filename) => { let version = filename.version.clone(); - let compatibility = Self::wheel_compatibility(&filename, tags, no_binary); + let compatibility = Self::wheel_compatibility( + &filename, + &file.hashes, + tags, + required_hashes, + no_binary, + ); let dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist { filename, file: Box::new(file), @@ -87,7 +99,12 @@ impl FlatIndex { } } DistFilename::SourceDistFilename(filename) => { - let compatibility = Self::source_dist_compatibility(&filename, no_build); + let compatibility = Self::source_dist_compatibility( + &filename, + &file.hashes, + required_hashes, + no_build, + ); let dist = Dist::Source(SourceDist::Registry(RegistrySourceDist { filename: filename.clone(), file: Box::new(file), @@ -107,6 +124,8 @@ impl FlatIndex { fn source_dist_compatibility( filename: &SourceDistFilename, + hashes: &[HashDigest], + required_hashes: &RequiredHashes, no_build: &NoBuild, ) -> SourceDistCompatibility { // Check if source distributions are allowed for this package. @@ -120,12 +139,28 @@ impl FlatIndex { return SourceDistCompatibility::Incompatible(IncompatibleSource::NoBuild); } + // Check if hashes line up + if let Some(required_hashes) = required_hashes.get(&filename.name) { + if !required_hashes.is_empty() { + if hashes.is_empty() { + return SourceDistCompatibility::Incompatible(IncompatibleSource::MissingHash); + } + if !hashes.iter().any(|hash| required_hashes.contains(hash)) { + return SourceDistCompatibility::Incompatible( + IncompatibleSource::MismatchedHash, + ); + } + } + } + SourceDistCompatibility::Compatible } fn wheel_compatibility( filename: &WheelFilename, + hashes: &[HashDigest], tags: &Tags, + required_hashes: &RequiredHashes, no_binary: &NoBinary, ) -> WheelCompatibility { // Check if binaries are allowed for this package. @@ -139,6 +174,18 @@ impl FlatIndex { return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary); } + // Check if hashes line up + if let Some(required_hashes) = required_hashes.get(&filename.name) { + if !required_hashes.is_empty() { + if hashes.is_empty() { + return WheelCompatibility::Incompatible(IncompatibleWheel::MissingHash); + } + if !hashes.iter().any(|hash| required_hashes.contains(hash)) { + return WheelCompatibility::Incompatible(IncompatibleWheel::MismatchedHash); + } + } + } + // Determine a compatibility for the wheel based on tags. WheelCompatibility::from(filename.compatibility(tags)) } diff --git a/crates/uv-resolver/src/hash_checking_mode.rs b/crates/uv-resolver/src/hash_checking_mode.rs new file mode 100644 index 000000000..080939339 --- /dev/null +++ b/crates/uv-resolver/src/hash_checking_mode.rs @@ -0,0 +1,15 @@ +#[derive(Debug, Default, Clone, Copy)] +pub enum HashCheckingMode { + /// Hash-checking mode is disabled. + #[default] + Disabled, + /// Hash-checking mode is enabled. + Enabled, +} + +impl HashCheckingMode { + /// Returns `true` if hash-checking is enabled. + pub fn is_enabled(self) -> bool { + matches!(self, Self::Enabled) + } +} diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index c7fd1aea1..4a1dad07f 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -2,6 +2,7 @@ pub use dependency_mode::DependencyMode; pub use error::ResolveError; pub use exclusions::Exclusions; pub use flat_index::FlatIndex; +pub use hash_checking_mode::HashCheckingMode; pub use manifest::Manifest; pub use options::{Options, OptionsBuilder}; pub use preferences::{Preference, PreferenceError}; @@ -26,6 +27,7 @@ mod editables; mod error; mod exclusions; mod flat_index; +mod hash_checking_mode; mod manifest; mod options; mod pins; diff --git a/crates/uv-resolver/src/options.rs b/crates/uv-resolver/src/options.rs index 764728391..bc9481fed 100644 --- a/crates/uv-resolver/src/options.rs +++ b/crates/uv-resolver/src/options.rs @@ -1,5 +1,6 @@ use chrono::{DateTime, Utc}; +use crate::hash_checking_mode::HashCheckingMode; use crate::{DependencyMode, PreReleaseMode, ResolutionMode}; /// Options for resolving a manifest. @@ -8,8 +9,8 @@ pub struct Options { pub resolution_mode: ResolutionMode, pub prerelease_mode: PreReleaseMode, pub dependency_mode: DependencyMode, + pub hash_checking_mode: HashCheckingMode, pub exclude_newer: Option>, - pub require_hashes: bool, } /// Builder for [`Options`]. @@ -18,8 +19,8 @@ pub struct OptionsBuilder { resolution_mode: ResolutionMode, prerelease_mode: PreReleaseMode, dependency_mode: DependencyMode, + hash_checking_mode: HashCheckingMode, exclude_newer: Option>, - require_hashes: bool, } impl OptionsBuilder { @@ -49,6 +50,13 @@ impl OptionsBuilder { self } + /// Sets the hash-checking mode. + #[must_use] + pub fn hash_checking_mode(mut self, hash_checking_mode: HashCheckingMode) -> Self { + self.hash_checking_mode = hash_checking_mode; + self + } + /// Sets the exclusion date. #[must_use] pub fn exclude_newer(mut self, exclude_newer: Option>) -> Self { @@ -56,21 +64,14 @@ impl OptionsBuilder { self } - /// Sets the `--requires-hash` flag. - #[must_use] - pub fn require_hashes(mut self, require_hashes: bool) -> Self { - self.require_hashes = require_hashes; - self - } - /// Builds the options. pub fn build(self) -> Options { Options { resolution_mode: self.resolution_mode, prerelease_mode: self.prerelease_mode, dependency_mode: self.dependency_mode, + hash_checking_mode: self.hash_checking_mode, exclude_newer: self.exclude_newer, - require_hashes: self.require_hashes, } } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 1a4d85354..732cdf867 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -31,12 +31,12 @@ use uv_configuration::{Constraints, Overrides}; use uv_distribution::DistributionDatabase; use uv_interpreter::Interpreter; use uv_normalize::PackageName; -use uv_types::{BuildContext, InstalledPackagesProvider}; +use uv_types::{BuildContext, InstalledPackagesProvider, RequiredHashes}; use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::editables::Editables; use crate::error::ResolveError; -use crate::flat_index::FlatIndex; +use crate::hash_checking_mode::HashCheckingMode; use crate::manifest::Manifest; use crate::pins::FilePins; use crate::preferences::Preferences; @@ -55,7 +55,7 @@ pub use crate::resolver::provider::{ use crate::resolver::reporter::Facade; pub use crate::resolver::reporter::{BuildId, Reporter}; use crate::yanks::AllowedYanks; -use crate::{DependencyMode, Exclusions, Options}; +use crate::{DependencyMode, Exclusions, FlatIndex, Options}; mod batch_prefetch; mod index; @@ -122,6 +122,8 @@ pub struct Resolver< urls: Urls, locals: Locals, dependency_mode: DependencyMode, + hash_checking_mode: HashCheckingMode, + hashes: &'a RequiredHashes, markers: &'a MarkerEnvironment, python_requirement: PythonRequirement, selector: CandidateSelector, @@ -156,6 +158,7 @@ impl< client: &'a RegistryClient, flat_index: &'a FlatIndex, index: &'a InMemoryIndex, + hashes: &'a RequiredHashes, build_context: &'a Context, installed_packages: &'a InstalledPackages, ) -> Result { @@ -166,6 +169,7 @@ impl< tags, PythonRequirement::new(interpreter, markers), AllowedYanks::from_manifest(&manifest, markers), + hashes, options.exclude_newer, build_context.no_binary(), build_context.no_build(), @@ -173,6 +177,7 @@ impl< Self::new_custom_io( manifest, options, + hashes, markers, PythonRequirement::new(interpreter, markers), index, @@ -189,9 +194,11 @@ impl< > Resolver<'a, Provider, InstalledPackages> { /// Initialize a new resolver using a user provided backend. + #[allow(clippy::too_many_arguments)] pub fn new_custom_io( manifest: Manifest, options: Options, + hashes: &'a RequiredHashes, markers: &'a MarkerEnvironment, python_requirement: PythonRequirement, index: &'a InMemoryIndex, @@ -205,6 +212,7 @@ impl< visited: DashSet::default(), selector: CandidateSelector::for_resolution(options, &manifest, markers), dependency_mode: options.dependency_mode, + hash_checking_mode: options.hash_checking_mode, urls: Urls::from_manifest(&manifest, markers)?, locals: Locals::from_manifest(&manifest, markers), project: manifest.project, @@ -214,6 +222,7 @@ impl< preferences: Preferences::from_iter(manifest.preferences, markers), exclusions: manifest.exclusions, editables: Editables::from_requirements(manifest.editables), + hashes, markers, python_requirement, reporter: None, @@ -518,6 +527,13 @@ impl< PubGrubPackage::Root(_) => {} PubGrubPackage::Python(_) => {} PubGrubPackage::Package(package_name, _extra, None) => { + // Validate that the package is permitted under hash-checking mode. + if self.hash_checking_mode.is_enabled() { + if !self.hashes.contains(package_name) { + return Err(ResolveError::UnhashedPackage(package_name.clone())); + } + } + // Emit a request to fetch the metadata for this package. if self.index.packages.register(package_name.clone()) { priorities.add(package_name.clone()); @@ -527,6 +543,13 @@ impl< } } PubGrubPackage::Package(package_name, _extra, Some(url)) => { + // Validate that the package is permitted under hash-checking mode. + if self.hash_checking_mode.is_enabled() { + if !self.hashes.contains(package_name) { + return Err(ResolveError::UnhashedPackage(package_name.clone())); + } + } + // Emit a request to fetch the metadata for this distribution. let dist = Dist::from_url(package_name.clone(), url.clone())?; if self.index.distributions.register(dist.package_id()) { diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 77078fd50..eb36edb03 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -3,14 +3,14 @@ use std::future::Future; use anyhow::Result; use chrono::{DateTime, Utc}; -use distribution_types::{Dist, IndexLocations}; +use distribution_types::{Dist, IndexLocations, Name}; use platform_tags::Tags; use pypi_types::Metadata23; use uv_client::RegistryClient; use uv_configuration::{NoBinary, NoBuild}; use uv_distribution::DistributionDatabase; use uv_normalize::PackageName; -use uv_types::BuildContext; +use uv_types::{BuildContext, RequiredHashes}; use crate::flat_index::FlatIndex; use crate::python_requirement::PythonRequirement; @@ -83,6 +83,7 @@ pub struct DefaultResolverProvider<'a, Context: BuildContext + Send + Sync> { tags: Tags, python_requirement: PythonRequirement, allowed_yanks: AllowedYanks, + required_hashes: RequiredHashes, exclude_newer: Option>, no_binary: NoBinary, no_build: NoBuild, @@ -98,6 +99,7 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex tags: &'a Tags, python_requirement: PythonRequirement, allowed_yanks: AllowedYanks, + required_hashes: &'a RequiredHashes, exclude_newer: Option>, no_binary: &'a NoBinary, no_build: &'a NoBuild, @@ -109,6 +111,7 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex tags: tags.clone(), python_requirement, allowed_yanks, + required_hashes: required_hashes.clone(), exclude_newer, no_binary: no_binary.clone(), no_build: no_build.clone(), @@ -136,6 +139,7 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider &self.tags, &self.python_requirement, &self.allowed_yanks, + &self.required_hashes, self.exclude_newer.as_ref(), self.flat_index.get(package_name).cloned(), &self.no_binary, @@ -175,7 +179,8 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider /// Fetch the metadata for a distribution, building it if necessary. async fn get_or_build_wheel_metadata<'io>(&'io self, dist: &'io Dist) -> WheelMetadataResult { - match self.fetcher.get_or_build_wheel_metadata(dist).await { + let hashes = self.required_hashes.get(dist.name()).unwrap_or_default(); + match self.fetcher.get_or_build_wheel_metadata(dist, hashes).await { Ok(metadata) => Ok(MetadataResponse::Found(metadata)), Err(err) => match err { uv_distribution::Error::Client(client) => match client.into_kind() { diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index f2608425e..2d714ca38 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -2,8 +2,9 @@ use std::collections::btree_map::{BTreeMap, Entry}; use std::sync::OnceLock; use chrono::{DateTime, Utc}; +use rkyv::{de::deserializers::SharedDeserializeMap, Deserialize}; use rustc_hash::FxHashSet; -use tracing::{instrument, warn}; +use tracing::instrument; use distribution_filename::{DistFilename, WheelFilename}; use distribution_types::{ @@ -13,10 +14,10 @@ use distribution_types::{ use pep440_rs::{Version, VersionSpecifiers}; use platform_tags::Tags; use pypi_types::{HashDigest, Yanked}; -use rkyv::{de::deserializers::SharedDeserializeMap, Deserialize}; use uv_client::{OwnedArchive, SimpleMetadata, VersionFiles}; use uv_configuration::{NoBinary, NoBuild}; use uv_normalize::PackageName; +use uv_types::RequiredHashes; use uv_warnings::warn_user_once; use crate::flat_index::FlatDistributions; @@ -47,6 +48,7 @@ impl VersionMap { tags: &Tags, python_requirement: &PythonRequirement, allowed_yanks: &AllowedYanks, + required_hashes: &RequiredHashes, exclude_newer: Option<&DateTime>, flat_index: Option, no_binary: &NoBinary, @@ -110,6 +112,10 @@ impl VersionMap { .allowed_versions(package_name) .cloned() .unwrap_or_default(); + let required_hashes = required_hashes + .get(package_name) + .unwrap_or_default() + .to_vec(); Self { inner: VersionMapInner::Lazy(VersionMapLazy { map, @@ -121,6 +127,7 @@ impl VersionMap { python_requirement: python_requirement.clone(), exclude_newer: exclude_newer.copied(), allowed_yanks, + required_hashes, }), } } @@ -303,6 +310,8 @@ struct VersionMapLazy { exclude_newer: Option>, /// Which yanked versions are allowed allowed_yanks: FxHashSet, + /// The hashes of allowed distributions. + required_hashes: Vec, } impl VersionMapLazy { @@ -386,6 +395,7 @@ impl VersionMapLazy { &filename, &version, requires_python, + &hashes, yanked, excluded, upload_time, @@ -401,6 +411,7 @@ impl VersionMapLazy { let compatibility = self.source_dist_compatibility( &version, requires_python, + &hashes, yanked, excluded, upload_time, @@ -423,10 +434,12 @@ impl VersionMapLazy { simple.dist.get_or_init(get_or_init).as_ref() } + #[allow(clippy::too_many_arguments)] fn source_dist_compatibility( &self, version: &Version, requires_python: Option, + hashes: &[HashDigest], yanked: Option, excluded: bool, upload_time: Option, @@ -443,6 +456,19 @@ impl VersionMapLazy { )); } + // Check if hashes line up + if !self.required_hashes.is_empty() { + if hashes.is_empty() { + return SourceDistCompatibility::Incompatible(IncompatibleSource::MissingHash); + } + if !hashes + .iter() + .any(|hash| self.required_hashes.contains(hash)) + { + return SourceDistCompatibility::Incompatible(IncompatibleSource::MismatchedHash); + } + } + // Check if yanked if let Some(yanked) = yanked { if yanked.is_yanked() && !self.allowed_yanks.contains(version) { @@ -466,11 +492,13 @@ impl VersionMapLazy { SourceDistCompatibility::Compatible } + #[allow(clippy::too_many_arguments)] fn wheel_compatibility( &self, filename: &WheelFilename, version: &Version, requires_python: Option, + hashes: &[HashDigest], yanked: Option, excluded: bool, upload_time: Option, @@ -485,6 +513,19 @@ impl VersionMapLazy { return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time)); } + // Check if hashes line up + if !self.required_hashes.is_empty() { + if hashes.is_empty() { + return WheelCompatibility::Incompatible(IncompatibleWheel::MissingHash); + } + if !hashes + .iter() + .any(|hash| self.required_hashes.contains(hash)) + { + return WheelCompatibility::Incompatible(IncompatibleWheel::MismatchedHash); + } + } + // Check if yanked if let Some(yanked) = yanked { if yanked.is_yanked() && !self.allowed_yanks.contains(version) { diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 16cb9e195..93ca66436 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -21,7 +21,9 @@ use uv_resolver::{ DisplayResolutionGraph, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, SourceBuildTrait}; +use uv_types::{ + BuildContext, BuildIsolation, EmptyInstalledPackages, RequiredHashes, SourceBuildTrait, +}; // Exclude any packages uploaded after this date. static EXCLUDE_NEWER: Lazy> = Lazy::new(|| { @@ -123,6 +125,7 @@ async fn resolve( find_default_python(&Cache::temp().unwrap()).expect("Expected a python to be installed"); let interpreter = Interpreter::artificial(real_interpreter.platform().clone(), markers.clone()); let build_context = DummyContext::new(Cache::temp()?, interpreter.clone()); + let hashes = RequiredHashes::default(); let installed_packages = EmptyInstalledPackages; let resolver = Resolver::new( manifest, @@ -133,6 +136,7 @@ async fn resolve( &client, &flat_index, &index, + &hashes, &build_context, &installed_packages, )?; diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index a4f22dd51..2ee16ce44 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -15,7 +15,9 @@ workspace = true [dependencies] distribution-types = { workspace = true } once-map = { workspace = true } +pep440_rs = { workspace = true } pep508_rs = { workspace = true } +pypi-types = { workspace = true } uv-cache = { workspace = true } uv-interpreter = { workspace = true } uv-normalize = { workspace = true } @@ -27,6 +29,7 @@ itertools = { workspace = true } rustc-hash = { workspace = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } +thiserror = { workspace = true } [features] default = [] diff --git a/crates/uv-types/src/hashes.rs b/crates/uv-types/src/hashes.rs new file mode 100644 index 000000000..4f12c18c7 --- /dev/null +++ b/crates/uv-types/src/hashes.rs @@ -0,0 +1,99 @@ +use rustc_hash::FxHashMap; +use std::str::FromStr; + +use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; +use pypi_types::{HashDigest, HashError}; +use uv_normalize::PackageName; + +/// A set of package versions that are permitted, even if they're marked as yanked by the +/// relevant index. +#[derive(Debug, Default, Clone)] +pub struct RequiredHashes(FxHashMap>); + +impl RequiredHashes { + /// Generate the [`RequiredHashes`] from a set of requirement entries. + pub fn from_requirements( + requirements: impl Iterator)>, + markers: &MarkerEnvironment, + ) -> Result { + let mut allowed_hashes = FxHashMap::>::default(); + + // For each requirement, map from name to allowed hashes. We use the last entry for each + // package. + // + // For now, unnamed requirements are unsupported. This should be fine, since `--require-hashes` + // tends to be used after `pip-compile`, which will always output named requirements. + // + // TODO(charlie): Preserve hashes from `requirements.txt` through to this pass, so that we + // can iterate over requirements directly, rather than iterating over the entries. + for (requirement, hashes) in requirements { + if !requirement.evaluate_markers(markers, &[]) { + continue; + } + + // Every requirement must be either a pinned version or a direct URL. + match requirement.version_or_url.as_ref() { + Some(VersionOrUrl::Url(_)) => { + // Direct URLs are always allowed. + } + Some(VersionOrUrl::VersionSpecifier(specifiers)) => { + if specifiers + .iter() + .any(|specifier| matches!(specifier.operator(), pep440_rs::Operator::Equal)) + { + // Pinned versions are allowed. + } else { + return Err(RequiredHashesError::UnpinnedRequirement( + requirement.to_string(), + )); + } + } + None => { + return Err(RequiredHashesError::UnpinnedRequirement( + requirement.to_string(), + )) + } + } + + // Every requirement must include a hash. + if hashes.is_empty() { + return Err(RequiredHashesError::MissingHashes(requirement.to_string())); + } + + // Parse the hashes. + let hashes = hashes + .iter() + .map(|hash| HashDigest::from_str(hash)) + .collect::, _>>() + .unwrap(); + + // TODO(charlie): Extract hashes from URL fragments. + allowed_hashes.insert(requirement.name, hashes); + } + + Ok(Self(allowed_hashes)) + } + + /// Returns versions for the given package which are allowed even if marked as yanked by the + /// relevant index. + pub fn get(&self, package_name: &PackageName) -> Option<&[HashDigest]> { + self.0.get(package_name).map(Vec::as_slice) + } + + /// Returns whether the given package is allowed even if marked as yanked by the relevant index. + pub fn contains(&self, package_name: &PackageName) -> bool { + self.0.contains_key(package_name) + } +} + +#[derive(thiserror::Error, Debug)] +pub enum RequiredHashesError { + #[error(transparent)] + Hash(#[from] HashError), + #[error("Unnamed requirements are not supported in `--require-hashes`")] + UnnamedRequirement, + #[error("In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: {0}")] + UnpinnedRequirement(String), + #[error("In `--require-hashes` mode, all requirement must have a hash, but none were provided for: {0}")] + MissingHashes(String), +} diff --git a/crates/uv-types/src/lib.rs b/crates/uv-types/src/lib.rs index cf28981fd..281e685a5 100644 --- a/crates/uv-types/src/lib.rs +++ b/crates/uv-types/src/lib.rs @@ -1,10 +1,12 @@ //! Fundamental types shared across `uv` crates. pub use builds::*; pub use downloads::*; +pub use hashes::*; pub use requirements::*; pub use traits::*; mod builds; mod downloads; +mod hashes; mod requirements; mod traits; diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 4b2235ec7..c1388e6b2 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -38,7 +38,7 @@ use uv_resolver::{ Manifest, OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, }; use uv_toolchain::PythonVersion; -use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight}; +use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight, RequiredHashes}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, ResolverReporter}; @@ -101,6 +101,7 @@ pub(crate) async fn pip_compile( // Read all requirements from the provided sources. let RequirementsSpecification { project, + entries: _, requirements, constraints, overrides, @@ -198,6 +199,9 @@ pub(crate) async fn pip_compile( |python_version| Cow::Owned(python_version.markers(interpreter.markers())), ); + // Don't enforce hashes during resolution. + let hashes = RequiredHashes::default(); + // Incorporate any index locations from the provided sources. let index_locations = index_locations.combine(index_url, extra_index_urls, find_links, no_index); @@ -229,7 +233,7 @@ pub(crate) async fn pip_compile( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, &tags, &no_build, &NoBinary::None) + FlatIndex::from_entries(entries, &tags, &hashes, &no_build, &NoBinary::None) }; // Track in-flight downloads, builds, etc., across resolutions. @@ -268,6 +272,7 @@ pub(crate) async fn pip_compile( // Convert from unnamed to named requirements. let mut requirements = NamedRequirementsResolver::new( requirements, + false, &build_dispatch, &client, &top_level_index, @@ -282,6 +287,7 @@ pub(crate) async fn pip_compile( SourceTreeResolver::new( source_trees, &extras, + false, &build_dispatch, &client, &top_level_index, @@ -306,7 +312,7 @@ pub(crate) async fn pip_compile( LocalEditable { url, path, extras } })); - let downloader = Downloader::new(&cache, &tags, &client, &build_dispatch) + let downloader = Downloader::new(&cache, &tags, &hashes, &client, &build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64)); // Build all editables. @@ -354,6 +360,7 @@ pub(crate) async fn pip_compile( &constraints, &overrides, &editables, + &hashes, &build_dispatch, &client, &top_level_index, @@ -370,7 +377,7 @@ pub(crate) async fn pip_compile( preferences, project, editables, - // Do not consider any installed packages during compilation + // Do not consider any installed packages during resolution. Exclusions::All, lookaheads, ); @@ -392,6 +399,7 @@ pub(crate) async fn pip_compile( &client, &flat_index, &top_level_index, + &hashes, &build_dispatch, &EmptyInstalledPackages, )? diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 2135634a5..040f2ba87 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -14,7 +14,7 @@ use distribution_types::{ LocalEditables, Name, Resolution, }; use install_wheel_rs::linker::LinkMode; -use pep508_rs::{MarkerEnvironment, Requirement}; +use pep508_rs::{MarkerEnvironment, Requirement, RequirementsTxtRequirement}; use platform_tags::Tags; use pypi_types::{Metadata23, Yanked}; use requirements_txt::EditableRequirement; @@ -37,10 +37,10 @@ use uv_requirements::{ RequirementsSpecification, SourceTreeResolver, }; use uv_resolver::{ - DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, OptionsBuilder, - PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, + DependencyMode, Exclusions, FlatIndex, HashCheckingMode, InMemoryIndex, Manifest, Options, + OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_types::{BuildIsolation, InFlight}; +use uv_types::{BuildIsolation, InFlight, RequiredHashes}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; @@ -85,10 +85,6 @@ pub(crate) async fn pip_install( ) -> Result { let start = std::time::Instant::now(); - if require_hashes { - warn_user!("Hash-checking mode (via `--require-hashes`) is not yet supported."); - } - let client_builder = BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls) @@ -97,6 +93,7 @@ pub(crate) async fn pip_install( // Read all requirements from the provided sources. let RequirementsSpecification { project, + entries, requirements, constraints, overrides, @@ -188,6 +185,21 @@ pub(crate) async fn pip_install( let tags = venv.interpreter().tags()?; let markers = venv.interpreter().markers(); + // Collect the set of required hashes. + let hashes = if require_hashes { + RequiredHashes::from_requirements( + entries + .into_iter() + .filter_map(|requirement| match requirement.requirement { + RequirementsTxtRequirement::Pep508(req) => Some((req, requirement.hashes)), + RequirementsTxtRequirement::Unnamed(_) => None, + }), + markers, + )? + } else { + RequiredHashes::default() + }; + // Incorporate any index locations from the provided sources. let index_locations = index_locations.combine(index_url, extra_index_urls, find_links, no_index); @@ -212,7 +224,7 @@ pub(crate) async fn pip_install( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, tags, &no_build, &no_binary) + FlatIndex::from_entries(entries, tags, &hashes, &no_build, &no_binary) }; // Determine whether to enable build isolation. @@ -252,19 +264,31 @@ pub(crate) async fn pip_install( // Resolve the requirements from the provided sources. let requirements = { // Convert from unnamed to named requirements. - let mut requirements = - NamedRequirementsResolver::new(requirements, &resolve_dispatch, &client, &index) - .with_reporter(ResolverReporter::from(printer)) - .resolve() - .await?; + let mut requirements = NamedRequirementsResolver::new( + requirements, + require_hashes, + &resolve_dispatch, + &client, + &index, + ) + .with_reporter(ResolverReporter::from(printer)) + .resolve() + .await?; // Resolve any source trees into requirements. if !source_trees.is_empty() { requirements.extend( - SourceTreeResolver::new(source_trees, extras, &resolve_dispatch, &client, &index) - .with_reporter(ResolverReporter::from(printer)) - .resolve() - .await?, + SourceTreeResolver::new( + source_trees, + extras, + require_hashes, + &resolve_dispatch, + &client, + &index, + ) + .with_reporter(ResolverReporter::from(printer)) + .resolve() + .await?, ); } @@ -282,6 +306,7 @@ pub(crate) async fn pip_install( build_editables( &editables, editable_wheel_dir.path(), + &hashes, &cache, &interpreter, tags, @@ -296,8 +321,12 @@ pub(crate) async fn pip_install( .resolution_mode(resolution_mode) .prerelease_mode(prerelease_mode) .dependency_mode(dependency_mode) + .hash_checking_mode(if require_hashes { + HashCheckingMode::Enabled + } else { + HashCheckingMode::Disabled + }) .exclude_newer(exclude_newer) - .require_hashes(require_hashes) .build(); // Resolve the requirements. @@ -307,6 +336,7 @@ pub(crate) async fn pip_install( overrides, project, &editables, + &hashes, &site_packages, &reinstall, &upgrade, @@ -367,6 +397,7 @@ pub(crate) async fn pip_install( link_mode, compile, &index_locations, + &hashes, tags, &client, &in_flight, @@ -442,6 +473,7 @@ async fn read_requirements( async fn build_editables( editables: &[EditableRequirement], editable_wheel_dir: &Path, + hashes: &RequiredHashes, cache: &Cache, interpreter: &Interpreter, tags: &Tags, @@ -451,7 +483,7 @@ async fn build_editables( ) -> Result, Error> { let start = std::time::Instant::now(); - let downloader = Downloader::new(cache, tags, client, build_dispatch) + let downloader = Downloader::new(cache, tags, hashes, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64)); let editables = LocalEditables::from_editables(editables.iter().map(|editable| { @@ -508,6 +540,7 @@ async fn resolve( overrides: Vec, project: Option, editables: &[BuiltEditable], + hashes: &RequiredHashes, site_packages: &SitePackages<'_>, reinstall: &Reinstall, upgrade: &Upgrade, @@ -554,6 +587,7 @@ async fn resolve( &constraints, &overrides, &editables, + hashes, build_dispatch, client, index, @@ -584,6 +618,7 @@ async fn resolve( client, flat_index, index, + hashes, build_dispatch, site_packages, )? @@ -627,6 +662,7 @@ async fn install( link_mode: LinkMode, compile: bool, index_urls: &IndexLocations, + hashes: &RequiredHashes, tags: &Tags, client: &RegistryClient, in_flight: &InFlight, @@ -654,6 +690,7 @@ async fn install( site_packages, reinstall, no_binary, + hashes, index_urls, cache, venv, @@ -706,7 +743,7 @@ async fn install( } else { let start = std::time::Instant::now(); - let downloader = Downloader::new(cache, tags, client, build_dispatch) + let downloader = Downloader::new(cache, tags, hashes, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64)); let wheels = downloader @@ -1022,6 +1059,9 @@ enum Error { #[error(transparent)] Platform(#[from] platform_tags::PlatformError), + #[error(transparent)] + RequiredHashes(#[from] uv_types::RequiredHashesError), + #[error(transparent)] Io(#[from] std::io::Error), diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 70a9f4998..093954682 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -10,6 +10,7 @@ use distribution_types::{ IndexLocations, InstalledMetadata, LocalDist, LocalEditable, LocalEditables, Name, ResolvedDist, }; use install_wheel_rs::linker::LinkMode; +use pep508_rs::RequirementsTxtRequirement; use platform_tags::Tags; use pypi_types::Yanked; use requirements_txt::EditableRequirement; @@ -29,8 +30,10 @@ use uv_requirements::{ ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification, SourceTreeResolver, }; -use uv_resolver::{DependencyMode, FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, Resolver}; -use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight}; +use uv_resolver::{ + DependencyMode, FlatIndex, HashCheckingMode, InMemoryIndex, Manifest, OptionsBuilder, Resolver, +}; +use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight, RequiredHashes}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; @@ -64,10 +67,6 @@ pub(crate) async fn pip_sync( ) -> Result { let start = std::time::Instant::now(); - if require_hashes { - warn_user!("Hash-checking mode (via `--require-hashes`) is not yet supported."); - } - let client_builder = BaseClientBuilder::new() .connectivity(connectivity) .native_tls(native_tls) @@ -76,6 +75,7 @@ pub(crate) async fn pip_sync( // Read all requirements from the provided sources. let RequirementsSpecification { project: _, + entries, requirements, constraints: _, overrides: _, @@ -135,6 +135,22 @@ pub(crate) async fn pip_sync( // Determine the current environment markers. let tags = venv.interpreter().tags()?; + let markers = venv.interpreter().markers(); + + // Collect the set of required hashes. + let hashes = if require_hashes { + RequiredHashes::from_requirements( + entries + .into_iter() + .filter_map(|requirement| match requirement.requirement { + RequirementsTxtRequirement::Pep508(req) => Some((req, requirement.hashes)), + RequirementsTxtRequirement::Unnamed(_) => None, + }), + markers, + )? + } else { + RequiredHashes::default() + }; // Incorporate any index locations from the provided sources. let index_locations = @@ -160,7 +176,7 @@ pub(crate) async fn pip_sync( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, tags, &no_build, &no_binary) + FlatIndex::from_entries(entries, tags, &hashes, &no_build, &no_binary) }; // Create a shared in-memory index. @@ -202,11 +218,16 @@ pub(crate) async fn pip_sync( // Convert from unnamed to named requirements. let requirements = { // Convert from unnamed to named requirements. - let mut requirements = - NamedRequirementsResolver::new(requirements, &build_dispatch, &client, &index) - .with_reporter(ResolverReporter::from(printer)) - .resolve() - .await?; + let mut requirements = NamedRequirementsResolver::new( + requirements, + require_hashes, + &build_dispatch, + &client, + &index, + ) + .with_reporter(ResolverReporter::from(printer)) + .resolve() + .await?; // Resolve any source trees into requirements. if !source_trees.is_empty() { @@ -214,6 +235,7 @@ pub(crate) async fn pip_sync( SourceTreeResolver::new( source_trees, &ExtrasSpecification::None, + require_hashes, &build_dispatch, &client, &index, @@ -232,6 +254,7 @@ pub(crate) async fn pip_sync( editables, &site_packages, reinstall, + &hashes, venv.interpreter(), tags, &cache, @@ -255,6 +278,7 @@ pub(crate) async fn pip_sync( site_packages, reinstall, &no_binary, + &hashes, &index_locations, &cache, &venv, @@ -293,7 +317,11 @@ pub(crate) async fn pip_sync( // Resolve with `--no-deps`. let options = OptionsBuilder::new() .dependency_mode(DependencyMode::Direct) - .require_hashes(require_hashes) + .hash_checking_mode(if require_hashes { + HashCheckingMode::Enabled + } else { + HashCheckingMode::Disabled + }) .build(); // Create a bound on the progress bar, since we know the number of packages upfront. @@ -309,6 +337,7 @@ pub(crate) async fn pip_sync( &client, &flat_index, &index, + &hashes, &build_dispatch, // TODO(zanieb): We should consider support for installed packages in pip sync &EmptyInstalledPackages, @@ -352,7 +381,7 @@ pub(crate) async fn pip_sync( } else { let start = std::time::Instant::now(); - let downloader = Downloader::new(&cache, tags, &client, &build_dispatch) + let downloader = Downloader::new(&cache, tags, &hashes, &client, &build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64)); let wheels = downloader @@ -548,6 +577,7 @@ async fn resolve_editables( editables: Vec, site_packages: &SitePackages<'_>, reinstall: &Reinstall, + hashes: &RequiredHashes, interpreter: &Interpreter, tags: &Tags, cache: &Cache, @@ -614,7 +644,7 @@ async fn resolve_editables( } else { let start = std::time::Instant::now(); - let downloader = Downloader::new(cache, tags, client, build_dispatch) + let downloader = Downloader::new(cache, tags, hashes, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(uninstalled.len() as u64)); let editables = LocalEditables::from_editables(uninstalled.iter().map(|editable| { diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 8c1f943fd..d90bcb414 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -21,7 +21,7 @@ use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder}; -use uv_types::{BuildContext, BuildIsolation, InFlight}; +use uv_types::{BuildContext, BuildIsolation, InFlight, RequiredHashes}; use crate::commands::ExitStatus; use crate::printer::Printer; @@ -167,7 +167,13 @@ async fn venv_impl( .fetch(index_locations.flat_index()) .await .map_err(VenvError::FlatIndex)?; - FlatIndex::from_entries(entries, tags, &NoBuild::All, &NoBinary::None) + FlatIndex::from_entries( + entries, + tags, + &RequiredHashes::default(), + &NoBuild::All, + &NoBinary::None, + ) }; // Create a shared in-memory index. diff --git a/crates/uv/tests/cache_prune.rs b/crates/uv/tests/cache_prune.rs index ee8f28c3a..4d96e6979 100644 --- a/crates/uv/tests/cache_prune.rs +++ b/crates/uv/tests/cache_prune.rs @@ -128,7 +128,7 @@ fn prune_stale_symlink() -> Result<()> { .success(); // Remove the wheels directory, causing the symlink to become stale. - let wheels = context.cache_dir.child("wheels-v0"); + let wheels = context.cache_dir.child("wheels-v1"); fs_err::remove_dir_all(wheels)?; let filters: Vec<_> = context diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 85a890dc1..dec0a147f 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -3756,3 +3756,204 @@ fn find_links_no_binary() -> Result<()> { Ok(()) } + +/// Provide the wrong hash with `--require-hashes`. +#[test] +fn require_hashes_mismatch() -> Result<()> { + let context = TestContext::new("3.12"); + + // Write to a requirements file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str( + "anyio==4.0.0 --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", + )?; + + // Raise an error. + uv_snapshot!(context.install() + .arg("-r") + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Omit a transitive dependency in `--require-hashes`. +#[test] +fn require_hashes_missing_dependency() -> Result<()> { + let context = TestContext::new("3.12"); + + // Write to a requirements file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str( + "anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", + )?; + + // Install without error when `--require-hashes` is omitted. + uv_snapshot!(context.install() + .arg("-r") + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: idna + "### + ); + + Ok(()) +} + +/// We disallow `--require-hashes` for editables' dependencies. +#[test] +fn require_hashes_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(&indoc::formatdoc! {r" + -e file://{workspace_root}/scripts/packages/black_editable[d] + ", + workspace_root = context.workspace_root.simplified_display(), + })?; + + // Install the editable packages. + uv_snapshot!(context.filters(), context.install() + .arg("-r") + .arg(requirements_txt.path()) + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Built 1 editable in [TIME] + error: In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: aiohttp + "### + ); + + Ok(()) +} + +/// If a hash is only included as a constraint, that's not good enough for `--require-hashes`. +#[test] +fn require_hashes_constraint() -> Result<()> { + let context = TestContext::new("3.12"); + + // Include the hash in the constraint file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==4.0.0")?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + // Install the editable packages. + uv_snapshot!(context.install() + .arg("-r") + .arg(requirements_txt.path()) + .arg("--require-hashes") + .arg("-c") + .arg(constraints_txt.path()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirement must have a hash, but none were provided for: anyio==4.0.0 + "### + ); + + // Include the hash in the requirements file, but pin the version in the constraint file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str( + "anyio --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", + )?; + + let constraints_txt = context.temp_dir.child("constraints.txt"); + constraints_txt.write_str("anyio==4.0.0")?; + + // Install the editable packages. + uv_snapshot!(context.install() + .arg("-r") + .arg(requirements_txt.path()) + .arg("--require-hashes") + .arg("-c") + .arg(constraints_txt.path()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: anyio + "### + ); + + Ok(()) +} + +/// If a hash is only included as a override, that's not good enough for `--require-hashes`. +/// +/// TODO(charlie): This _should_ be allowed. It's a bug. +#[test] +fn require_hashes_override() -> Result<()> { + let context = TestContext::new("3.12"); + + // Include the hash in the override file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==4.0.0")?; + + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + // Install the editable packages. + uv_snapshot!(context.install() + .arg("-r") + .arg(requirements_txt.path()) + .arg("--require-hashes") + .arg("--override") + .arg(overrides_txt.path()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirement must have a hash, but none were provided for: anyio==4.0.0 + "### + ); + + // Include the hash in the requirements file, but pin the version in the override file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str( + "anyio --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", + )?; + + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("anyio==4.0.0")?; + + // Install the editable packages. + uv_snapshot!(context.install() + .arg("-r") + .arg(requirements_txt.path()) + .arg("--require-hashes") + .arg("--override") + .arg(overrides_txt.path()), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: anyio + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 74ba1bf3a..387fc25a5 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -3081,3 +3081,1389 @@ requires-python = "<=3.5" Ok(()) } + +/// Omit the hash with `--require-hashes`. +#[test] +fn require_hashes_missing_hash() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==4.0.0")?; + + // Install without error when `--require-hashes` is omitted. + uv_snapshot!(command(&context) + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 + "### + ); + + // Error when `--require-hashes` is provided. + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirement must have a hash, but none were provided for: anyio==4.0.0 + "### + ); + + Ok(()) +} + +/// Omit the version with `--require-hashes`. +#[test] +fn require_hashes_missing_version() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str( + "anyio --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", + )?; + + // Install without error when `--require-hashes` is omitted. + uv_snapshot!(command(&context) + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.3.0 + "### + ); + + // Error when `--require-hashes` is provided. + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: anyio + "### + ); + + Ok(()) +} + +/// Include the hash for _just_ the wheel with `--no-binary`. +#[test] +fn require_hashes_wheel_no_binary() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--no-binary") + .arg(":all:") + .arg("--require-hashes"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include the hash for _just_ the wheel with `--only-binary`. +#[test] +fn require_hashes_wheel_only_binary() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--only-binary") + .arg(":all:") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 + "### + ); + + Ok(()) +} + +/// Include the hash for _just_ the source distribution with `--no-binary`. +#[test] +fn require_hashes_source_no_binary() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--no-binary") + .arg(":all:") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 + "### + ); + + Ok(()) +} + +/// Include the hash for _just_ the source distribution, with `--binary-only`. +#[test] +fn require_hashes_source_only_binary() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--only-binary") + .arg(":all:") + .arg("--require-hashes"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because anyio==4.0.0 is unusable because no wheels are usable and building from source is disabled and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include the correct hash algorithm, but the wrong digest. +#[test] +fn require_hashes_wrong_digest() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include the correct hash, but the wrong algorithm. +#[test] +fn require_hashes_wrong_algorithm() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha512:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Include the hash for a source distribution specified as a direct URL dependency. +#[test] +fn require_hashes_source_url() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio @ https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz) + "### + ); + + // Reinstall with the right hash, and verify that it's reused. + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - anyio==4.0.0 (from https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz) + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz) + "### + ); + + // Reinstall with the wrong hash, and verify that it's rejected despite being cached. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio @ https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz --hash=sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download and build: anyio @ https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz + Caused by: Hash mismatch for anyio @ https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz + + Expected: + sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + + Computed: + sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + "### + ); + + Ok(()) +} + +/// Include the _wrong_ hash for a source distribution specified as a direct URL dependency. +#[test] +fn require_hashes_source_url_mismatch() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio @ https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz --hash=sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download and build: anyio @ https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz + Caused by: Hash mismatch for anyio @ https://files.pythonhosted.org/packages/74/17/5075225ee1abbb93cd7fc30a2d343c6a3f5f71cf388f14768a7a38256581/anyio-4.0.0.tar.gz + + Expected: + sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + + Computed: + sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + "### + ); + + Ok(()) +} + +/// Include the hash for a built distribution specified as a direct URL dependency. +#[test] +fn require_hashes_wheel_url() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + "### + ); + + // Reinstall with the right hash, and verify that it's reused. + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + "### + ); + + // Reinstall with the wrong hash, and verify that it's rejected despite being cached. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl + Caused by: Hash mismatch for anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl + + Expected: + sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + + Computed: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + "### + ); + + // Sync a new dependency and include the wrong hash for anyio. Verify that we reuse anyio + // despite the wrong hash, like pip, since we don't validate hashes for already-installed + // distributions. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f\niniconfig==2.0.0 --hash=sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + iniconfig==2.0.0 + "### + ); + + Ok(()) +} + +/// Include the _wrong_ hash for a built distribution specified as a direct URL dependency. +#[test] +fn require_hashes_wheel_url_mismatch() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl + Caused by: Hash mismatch for anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl + + Expected: + sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + + Computed: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + "### + ); + + Ok(()) +} + +/// Reject Git dependencies when `--require-hashes` is provided. +#[test] +fn require_hashes_git() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio @ git+https://github.com/agronholm/anyio@4a23745badf5bf5ef7928f1e346e9986bd696d82 --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to download and build: anyio @ git+https://github.com/agronholm/anyio@4a23745badf5bf5ef7928f1e346e9986bd696d82 + Caused by: Hash-checking is not supported for Git repositories: anyio @ git+https://github.com/agronholm/anyio@4a23745badf5bf5ef7928f1e346e9986bd696d82 + "### + ); + + Ok(()) +} + +/// Reject local directory dependencies when `--require-hashes` is provided. +#[test] +fn require_hashes_source_tree() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(&format!( + "black @ {} --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a", + context + .workspace_root + .join("scripts/packages/black_editable") + .display() + ))?; + + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to build: black @ file://[WORKSPACE]/scripts/packages/black_editable + Caused by: Hash-checking is not supported for local directories: black @ file://[WORKSPACE]/scripts/packages/black_editable + "### + ); + + Ok(()) +} + +/// Include the hash for _just_ the wheel with `--only-binary`. +#[test] +fn require_hashes_re_download() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("anyio==4.0.0")?; + + // Install without `--require-hashes`. + uv_snapshot!(command(&context) + .arg("requirements.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 + "### + ); + + // Reinstall with `--require-hashes`, and the wrong hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + // Reinstall with `--require-hashes`, and the right hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - anyio==4.0.0 + + anyio==4.0.0 + "### + ); + + Ok(()) +} + +/// Include the hash for a built distribution specified as a local path dependency. +#[test] +fn require_hashes_wheel_path() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(&format!( + "tqdm @ {} --hash=sha256:a34996d4bd5abb2336e14ff0a2d22b92cfd0f0ed344e6883041ce01953276a13", + context + .workspace_root + .join("scripts/links/tqdm-1000.0.0-py3-none-any.whl") + .display() + ))?; + + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + tqdm==1000.0.0 (from file://[WORKSPACE]/scripts/links/tqdm-1000.0.0-py3-none-any.whl) + "### + ); + + Ok(()) +} + +/// Include the _wrong_ hash for a built distribution specified as a local path dependency. +#[test] +fn require_hashes_wheel_path_mismatch() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(&format!( + "tqdm @ {} --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", + context + .workspace_root + .join("scripts/links/tqdm-1000.0.0-py3-none-any.whl") + .display() + ))?; + + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: tqdm @ file://[WORKSPACE]/scripts/links/tqdm-1000.0.0-py3-none-any.whl + Caused by: Hash mismatch for tqdm @ file://[WORKSPACE]/scripts/links/tqdm-1000.0.0-py3-none-any.whl + + Expected: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + + Computed: + sha256:a34996d4bd5abb2336e14ff0a2d22b92cfd0f0ed344e6883041ce01953276a13 + "### + ); + + Ok(()) +} + +/// Include the hash for a source distribution specified as a local path dependency. +#[test] +fn require_hashes_source_path() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(&format!( + "tqdm @ {} --hash=sha256:89fa05cffa7f457658373b85de302d24d0c205ceda2819a8739e324b75e9430b", + context + .workspace_root + .join("scripts/links/tqdm-999.0.0.tar.gz") + .display() + ))?; + + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + tqdm==999.0.0 (from file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz) + "### + ); + + Ok(()) +} + +/// Include the _wrong_ hash for a source distribution specified as a local path dependency. +#[test] +fn require_hashes_source_path_mismatch() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(&format!( + "tqdm @ {} --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f", + context + .workspace_root + .join("scripts/links/tqdm-999.0.0.tar.gz") + .display() + ))?; + + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to build: tqdm @ file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz + Caused by: Hash mismatch for tqdm @ file://[WORKSPACE]/scripts/links/tqdm-999.0.0.tar.gz + + Expected: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + + Computed: + sha256:89fa05cffa7f457658373b85de302d24d0c205ceda2819a8739e324b75e9430b + "### + ); + + Ok(()) +} + +/// `--require-hashes` isn't supported for unnamed requirements (yet). +#[test] +fn require_hashes_unnamed() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("https://foo.com --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Unnamed requirements are not supported with `--require-hashes` + "### + ); + + Ok(()) +} + +/// We allow `--require-hashes` for editables, as long as no dependencies are included. +#[test] +fn require_hashes_editable() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(&indoc::formatdoc! {r" + -e file://{workspace_root}/scripts/packages/black_editable[d] + ", + workspace_root = context.workspace_root.simplified_display(), + })?; + + // Install the editable packages. + uv_snapshot!(context.filters(), command(&context) + .arg(requirements_txt.path()) + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Built 1 editable in [TIME] + Installed 1 package in [TIME] + + black==0.1.0 (from file://[WORKSPACE]/scripts/packages/black_editable) + "### + ); + + Ok(()) +} + +/// If a dependency is repeated, the hash should be required for both instances. +#[test] +fn require_hashes_repeated_dependency() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a\nanyio")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: anyio + "### + ); + + // Reverse the order. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio\nanyio==4.0.0 --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: anyio + "### + ); + + Ok(()) +} + +/// If a dependency is repeated, use the last hash provided. pip seems to use the _first_ hash. +#[test] +fn require_hashes_repeated_hash() -> Result<()> { + let context = TestContext::new("3.12"); + + // Use the same hash in both cases. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str(indoc::indoc! { r" + anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + " })?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + "### + ); + + // Use a different hash, but both are correct. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str(indoc::indoc! { r" + anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2 + " })?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes") + .arg("--reinstall"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + "### + ); + + // Use a different hash. The first hash is wrong, but that's fine, since we use the last hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str(indoc::indoc! { r" + anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:a7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=md5:420d85e19168705cdf0223621b18831a + " })?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes") + .arg("--reinstall"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + "### + ); + + // Use a different hash. The second hash is wrong. This should fail, since we use the last hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str(indoc::indoc! { r" + anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=md5:520d85e19168705cdf0223621b18831a + " })?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes") + .arg("--reinstall"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl + Caused by: Hash mismatch for anyio @ https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl + + Expected: + md5:520d85e19168705cdf0223621b18831a + + Computed: + md5:420d85e19168705cdf0223621b18831a + "### + ); + + Ok(()) +} + +/// If a dependency is repeated, the hash should be required for both instances. +#[test] +fn require_hashes_at_least_one() -> Result<()> { + let context = TestContext::new("3.12"); + + // Request `anyio` with a `sha256` hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 + "### + ); + + // Reinstall, requesting both `sha256` and `sha512`. We should reinstall from the cache, since + // at least one hash matches. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a --hash=md5:420d85e19168705cdf0223621b18831a")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - anyio==4.0.0 + + anyio==4.0.0 + "### + ); + + // This should be true even if the second hash is wrong. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("anyio==4.0.0 --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a --hash=md5:1234")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - anyio==4.0.0 + + anyio==4.0.0 + "### + ); + + Ok(()) +} + +/// Using `--find-links`, but the registry doesn't provide us with a hash. +#[test] +fn require_hashes_find_links_no_hash() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/no-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because it has no hash and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Using `--find-links`, and the registry serves us a correct hash. +#[test] +fn require_hashes_find_links_valid_hash() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/valid-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + example-a-961b4c22==1.0.0 + "### + ); + + Ok(()) +} + +/// Using `--find-links`, and the registry serves us an incorrect hash. +#[test] +fn require_hashes_find_links_invalid_hash() -> Result<()> { + let context = TestContext::new("3.12"); + + // First, request some other hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("example-a-961b4c22==1.0.0 --hash=sha256:123")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/invalid-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because the hash does not match and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + // Second, request the invalid hash, that the registry _thinks_ is correct. We should reject it. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:8838f9d005ff0432b258ba648d9cabb1cbdf06ac29d14f788b02edae544032ea")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/invalid-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + Caused by: Hash mismatch for example-a-961b4c22==1.0.0 + + Expected: + sha256:8838f9d005ff0432b258ba648d9cabb1cbdf06ac29d14f788b02edae544032ea + + Computed: + sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e + "### + ); + + // Third, request the correct hash, that the registry _thinks_ is correct. We should accept + // it, since it's already cached under this hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/invalid-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed 1 package in [TIME] + + example-a-961b4c22==1.0.0 + "### + ); + + // Fourth, request the correct hash, that the registry _thinks_ is correct, but without the + // cache. We _should_ accept it, but we currently don't. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--refresh") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/invalid-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because the hash does not match and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + // Finally, request the correct hash, along with the incorrect hash for the source distribution. + // Resolution will fail, since the incorrect hash matches the registry's hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e --hash=sha256:a3cf07a05aac526131a2e8b6e4375ee6c6eaac8add05b88035e960ac6cd999ee")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--refresh") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/invalid-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + Caused by: Hash mismatch for example-a-961b4c22==1.0.0 + + Expected: + sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e + sha256:a3cf07a05aac526131a2e8b6e4375ee6c6eaac8add05b88035e960ac6cd999ee + + Computed: + sha256:294e788dbe500fdc39e8b88e82652ab67409a1dc9dd06543d0fe0ae31b713eb3 + "### + ); + + Ok(()) +} + +/// Using `--index-url`, but the registry doesn't provide us with a hash. +#[test] +fn require_hashes_registry_no_hash() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes") + .arg("--index-url") + .arg("https://astral-test.github.io/astral-test-hash/no-hash/simple-html/"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because it has no hash and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Using `--index-url`, and the registry serves us a correct hash. +#[test] +fn require_hashes_registry_valid_hash() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://astral-test.github.io/astral-test-hash/valid-hash/simple-html/"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because example-a-961b4c22==1.0.0 was not found in the package registry and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + Ok(()) +} + +/// Using `--index-url`, and the registry serves us an incorrect hash. +#[test] +fn require_hashes_registry_invalid_hash() -> Result<()> { + let context = TestContext::new("3.12"); + + // First, request some other hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("example-a-961b4c22==1.0.0 --hash=sha256:123")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--index-url") + .arg("https://astral-test.github.io/astral-test-hash/invalid-hash/simple-html/"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because the hash does not match and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + // Second, request the invalid hash, that the registry _thinks_ is correct. We should reject it. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:8838f9d005ff0432b258ba648d9cabb1cbdf06ac29d14f788b02edae544032ea")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--index-url") + .arg("https://astral-test.github.io/astral-test-hash/invalid-hash/simple-html/"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + Caused by: Hash mismatch for example-a-961b4c22==1.0.0 + + Expected: + sha256:8838f9d005ff0432b258ba648d9cabb1cbdf06ac29d14f788b02edae544032ea + + Computed: + sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e + "### + ); + + // Third, request the correct hash, that the registry _thinks_ is correct. We should accept + // it, since it's already cached under this hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--index-url") + .arg("https://astral-test.github.io/astral-test-hash/invalid-hash/simple-html/"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Installed 1 package in [TIME] + + example-a-961b4c22==1.0.0 + "### + ); + + // Fourth, request the correct hash, that the registry _thinks_ is correct, but without the + // cache. We _should_ accept it, but we currently don't. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--refresh") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--index-url") + .arg("https://astral-test.github.io/astral-test-hash/invalid-hash/simple-html/"), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because the hash does not match and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + "### + ); + + // Finally, request the correct hash, along with the incorrect hash for the source distribution. + // Resolution will fail, since the incorrect hash matches the registry's hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e --hash=sha256:a3cf07a05aac526131a2e8b6e4375ee6c6eaac8add05b88035e960ac6cd999ee")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--refresh") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--index-url") + .arg("https://astral-test.github.io/astral-test-hash/invalid-hash/simple-html/"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + Caused by: Hash mismatch for example-a-961b4c22==1.0.0 + + Expected: + sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e + sha256:a3cf07a05aac526131a2e8b6e4375ee6c6eaac8add05b88035e960ac6cd999ee + + Computed: + sha256:294e788dbe500fdc39e8b88e82652ab67409a1dc9dd06543d0fe0ae31b713eb3 + "### + ); + + Ok(()) +} From c18551fd3c5604e31b6c22316e7d5b4fb1cf1627 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 15:19:47 -0400 Subject: [PATCH 092/110] Fall back to distributions without hashes in resolver (#2949) ## Summary This represents a change to `--require-hashes` in the event that we don't find a matching hash from the registry. The behavior in this PR is closer to pip's. Prior to this PR, if a distribution had no reported hash, or only mismatched hashes, we would mark it as incompatible. Now, we mark it as compatible, but we use the hash-agreement as part of the ordering, such that we prefer any distribution with a matching hash, then any distribution with no hash, then any distribution with a mismatched hash. As a result, if an index reports incorrect hashes, but the user provides the correct one, resolution now succeeds, where it would've failed. Similarly, if an index omits hashes altogether, but the user provides the correct one, resolution now succeeds, where it would've failed. If we end up picking a distribution whose hash ultimately doesn't match, we'll reject it later, after resolution. --- .../src/prioritized_distribution.rs | 128 ++++------ crates/uv-resolver/src/flat_index.rs | 61 +++-- crates/uv-resolver/src/version_map.rs | 75 +++--- crates/uv/tests/pip_install.rs | 5 +- crates/uv/tests/pip_sync.rs | 237 ++++++++++++++---- 5 files changed, 321 insertions(+), 185 deletions(-) diff --git a/crates/distribution-types/src/prioritized_distribution.rs b/crates/distribution-types/src/prioritized_distribution.rs index ac793de84..0a13a4b28 100644 --- a/crates/distribution-types/src/prioritized_distribution.rs +++ b/crates/distribution-types/src/prioritized_distribution.rs @@ -1,7 +1,7 @@ use std::fmt::{Display, Formatter}; use pep440_rs::VersionSpecifiers; -use platform_tags::{IncompatibleTag, TagCompatibility, TagPriority}; +use platform_tags::{IncompatibleTag, TagPriority}; use pypi_types::{HashDigest, Yanked}; use crate::{Dist, InstalledDist, ResolvedDistRef}; @@ -84,8 +84,6 @@ impl Display for IncompatibleDist { IncompatibleWheel::RequiresPython(python) => { write!(f, "it requires at python {python}") } - IncompatibleWheel::MissingHash => f.write_str("it has no hash"), - IncompatibleWheel::MismatchedHash => f.write_str("the hash does not match"), }, Self::Source(incompatibility) => match incompatibility { IncompatibleSource::NoBuild => { @@ -106,8 +104,6 @@ impl Display for IncompatibleDist { IncompatibleSource::RequiresPython(python) => { write!(f, "it requires python {python}") } - IncompatibleSource::MissingHash => f.write_str("it has no hash"), - IncompatibleSource::MismatchedHash => f.write_str("the hash does not match"), }, Self::Unavailable => f.write_str("no distributions are available"), } @@ -117,7 +113,7 @@ impl Display for IncompatibleDist { #[derive(Debug, Clone, PartialEq, Eq)] pub enum WheelCompatibility { Incompatible(IncompatibleWheel), - Compatible(TagPriority), + Compatible(Hash, TagPriority), } #[derive(Debug, PartialEq, Eq, Clone)] @@ -126,15 +122,13 @@ pub enum IncompatibleWheel { Tag(IncompatibleTag), RequiresPython(VersionSpecifiers), Yanked(Yanked), - MissingHash, - MismatchedHash, NoBinary, } #[derive(Debug, Clone, PartialEq, Eq)] pub enum SourceDistCompatibility { Incompatible(IncompatibleSource), - Compatible, + Compatible(Hash), } #[derive(Debug, PartialEq, Eq, Clone)] @@ -142,11 +136,19 @@ pub enum IncompatibleSource { ExcludeNewer(Option), RequiresPython(VersionSpecifiers), Yanked(Yanked), - MissingHash, - MismatchedHash, NoBuild, } +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub enum Hash { + /// The hash is present, but does not match the expected value. + Mismatched, + /// The hash is missing. + Missing, + /// The hash matches the expected value. + Matched, +} + impl PrioritizedDist { /// Create a new [`PrioritizedDist`] from the given wheel distribution. pub fn from_built( @@ -215,8 +217,19 @@ impl PrioritizedDist { /// Return the highest-priority distribution for the package version, if any. pub fn get(&self) -> Option { match (&self.0.wheel, &self.0.source) { + // If both are compatible, break ties based on the hash. + ( + Some((wheel, WheelCompatibility::Compatible(wheel_hash, tag_priority))), + Some((source_dist, SourceDistCompatibility::Compatible(source_hash))), + ) => { + if source_hash > wheel_hash { + Some(CompatibleDist::SourceDist(source_dist)) + } else { + Some(CompatibleDist::CompatibleWheel(wheel, *tag_priority)) + } + } // Prefer the highest-priority, platform-compatible wheel. - (Some((wheel, WheelCompatibility::Compatible(tag_priority))), _) => { + (Some((wheel, WheelCompatibility::Compatible(_, tag_priority))), _) => { Some(CompatibleDist::CompatibleWheel(wheel, *tag_priority)) } // If we have a compatible source distribution and an incompatible wheel, return the @@ -225,58 +238,36 @@ impl PrioritizedDist { // using the wheel is faster. ( Some((wheel, WheelCompatibility::Incompatible(_))), - Some((source_dist, SourceDistCompatibility::Compatible)), + Some((source_dist, SourceDistCompatibility::Compatible(_))), ) => Some(CompatibleDist::IncompatibleWheel { source_dist, wheel }), // Otherwise, if we have a source distribution, return it. - (None, Some((source_dist, SourceDistCompatibility::Compatible))) => { + (None, Some((source_dist, SourceDistCompatibility::Compatible(_)))) => { Some(CompatibleDist::SourceDist(source_dist)) } _ => None, } } - /// Return the compatible source distribution, if any. - pub fn compatible_source(&self) -> Option<&Dist> { - self.0 - .source - .as_ref() - .and_then(|(dist, compatibility)| match compatibility { - SourceDistCompatibility::Compatible => Some(dist), - SourceDistCompatibility::Incompatible(_) => None, - }) - } - /// Return the incompatible source distribution, if any. pub fn incompatible_source(&self) -> Option<(&Dist, &IncompatibleSource)> { self.0 .source .as_ref() .and_then(|(dist, compatibility)| match compatibility { - SourceDistCompatibility::Compatible => None, + SourceDistCompatibility::Compatible(_) => None, SourceDistCompatibility::Incompatible(incompatibility) => { Some((dist, incompatibility)) } }) } - /// Return the compatible built distribution, if any. - pub fn compatible_wheel(&self) -> Option<(&Dist, TagPriority)> { - self.0 - .wheel - .as_ref() - .and_then(|(dist, compatibility)| match compatibility { - WheelCompatibility::Compatible(priority) => Some((dist, *priority)), - WheelCompatibility::Incompatible(_) => None, - }) - } - /// Return the incompatible built distribution, if any. pub fn incompatible_wheel(&self) -> Option<(&Dist, &IncompatibleWheel)> { self.0 .wheel .as_ref() .and_then(|(dist, compatibility)| match compatibility { - WheelCompatibility::Compatible(_) => None, + WheelCompatibility::Compatible(_, _) => None, WheelCompatibility::Incompatible(incompatibility) => Some((dist, incompatibility)), }) } @@ -335,7 +326,7 @@ impl<'a> CompatibleDist<'a> { impl WheelCompatibility { pub fn is_compatible(&self) -> bool { - matches!(self, Self::Compatible(_)) + matches!(self, Self::Compatible(_, _)) } /// Return `true` if the current compatibility is more compatible than another. @@ -344,11 +335,12 @@ impl WheelCompatibility { /// Compatible wheel ordering is determined by tag priority. pub fn is_more_compatible(&self, other: &Self) -> bool { match (self, other) { - (Self::Compatible(_), Self::Incompatible(_)) => true, - (Self::Compatible(tag_priority), Self::Compatible(other_tag_priority)) => { - tag_priority > other_tag_priority - } - (Self::Incompatible(_), Self::Compatible(_)) => false, + (Self::Compatible(_, _), Self::Incompatible(_)) => true, + ( + Self::Compatible(hash, tag_priority), + Self::Compatible(other_hash, other_tag_priority), + ) => (hash, tag_priority) > (other_hash, other_tag_priority), + (Self::Incompatible(_), Self::Compatible(_, _)) => false, (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => { incompatibility.is_more_compatible(other_incompatibility) } @@ -364,9 +356,11 @@ impl SourceDistCompatibility { /// Incompatible source distribution priority selects a source distribution that was "closest" to being usable. pub fn is_more_compatible(&self, other: &Self) -> bool { match (self, other) { - (Self::Compatible, Self::Incompatible(_)) => true, - (Self::Compatible, Self::Compatible) => false, // Arbitrary - (Self::Incompatible(_), Self::Compatible) => false, + (Self::Compatible(_), Self::Incompatible(_)) => true, + (Self::Compatible(compatibility), Self::Compatible(other_compatibility)) => { + compatibility > other_compatibility + } + (Self::Incompatible(_), Self::Compatible(_)) => false, (Self::Incompatible(incompatibility), Self::Incompatible(other_incompatibility)) => { incompatibility.is_more_compatible(other_incompatibility) } @@ -374,41 +368,26 @@ impl SourceDistCompatibility { } } -impl From for WheelCompatibility { - fn from(value: TagCompatibility) -> Self { - match value { - TagCompatibility::Compatible(priority) => Self::Compatible(priority), - TagCompatibility::Incompatible(tag) => Self::Incompatible(IncompatibleWheel::Tag(tag)), - } - } -} - impl IncompatibleSource { fn is_more_compatible(&self, other: &Self) -> bool { match self { Self::ExcludeNewer(timestamp_self) => match other { // Smaller timestamps are closer to the cut-off time Self::ExcludeNewer(timestamp_other) => timestamp_other < timestamp_self, - Self::NoBuild - | Self::RequiresPython(_) - | Self::Yanked(_) - | Self::MissingHash - | Self::MismatchedHash => true, + Self::NoBuild | Self::RequiresPython(_) | Self::Yanked(_) => true, }, Self::RequiresPython(_) => match other { Self::ExcludeNewer(_) => false, // Version specifiers cannot be reasonably compared Self::RequiresPython(_) => false, - Self::NoBuild | Self::Yanked(_) | Self::MissingHash | Self::MismatchedHash => true, + Self::NoBuild | Self::Yanked(_) => true, }, Self::Yanked(_) => match other { Self::ExcludeNewer(_) | Self::RequiresPython(_) => false, // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), - Self::NoBuild | Self::MissingHash | Self::MismatchedHash => true, + Self::NoBuild => true, }, - Self::MissingHash => false, - Self::MismatchedHash => false, Self::NoBuild => false, } } @@ -426,37 +405,26 @@ impl IncompatibleWheel { timestamp_other < timestamp_self } }, - Self::NoBinary - | Self::RequiresPython(_) - | Self::Tag(_) - | Self::Yanked(_) - | Self::MissingHash - | Self::MismatchedHash => true, + Self::NoBinary | Self::RequiresPython(_) | Self::Tag(_) | Self::Yanked(_) => true, }, Self::Tag(tag_self) => match other { Self::ExcludeNewer(_) => false, Self::Tag(tag_other) => tag_other > tag_self, - Self::NoBinary - | Self::RequiresPython(_) - | Self::Yanked(_) - | Self::MissingHash - | Self::MismatchedHash => true, + Self::NoBinary | Self::RequiresPython(_) | Self::Yanked(_) => true, }, Self::RequiresPython(_) => match other { Self::ExcludeNewer(_) | Self::Tag(_) => false, // Version specifiers cannot be reasonably compared Self::RequiresPython(_) => false, - Self::NoBinary | Self::Yanked(_) | Self::MissingHash | Self::MismatchedHash => true, + Self::NoBinary | Self::Yanked(_) => true, }, Self::Yanked(_) => match other { Self::ExcludeNewer(_) | Self::Tag(_) | Self::RequiresPython(_) => false, // Yanks with a reason are more helpful for errors Self::Yanked(yanked_other) => matches!(yanked_other, Yanked::Reason(_)), - Self::NoBinary | Self::MissingHash | Self::MismatchedHash => true, + Self::NoBinary => true, }, Self::NoBinary => false, - Self::MismatchedHash => false, - Self::MissingHash => false, } } } diff --git a/crates/uv-resolver/src/flat_index.rs b/crates/uv-resolver/src/flat_index.rs index 3e09689bc..f11f9a2f7 100644 --- a/crates/uv-resolver/src/flat_index.rs +++ b/crates/uv-resolver/src/flat_index.rs @@ -6,11 +6,11 @@ use tracing::instrument; use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use distribution_types::{ - BuiltDist, Dist, File, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist, + BuiltDist, Dist, File, Hash, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist, RegistryBuiltDist, RegistrySourceDist, SourceDist, SourceDistCompatibility, WheelCompatibility, }; use pep440_rs::Version; -use platform_tags::Tags; +use platform_tags::{TagCompatibility, Tags}; use pypi_types::HashDigest; use uv_client::FlatIndexEntries; use uv_configuration::{NoBinary, NoBuild}; @@ -140,20 +140,19 @@ impl FlatIndex { } // Check if hashes line up - if let Some(required_hashes) = required_hashes.get(&filename.name) { - if !required_hashes.is_empty() { - if hashes.is_empty() { - return SourceDistCompatibility::Incompatible(IncompatibleSource::MissingHash); - } - if !hashes.iter().any(|hash| required_hashes.contains(hash)) { - return SourceDistCompatibility::Incompatible( - IncompatibleSource::MismatchedHash, - ); - } + let hash = if let Some(required_hashes) = required_hashes.get(&filename.name) { + if hashes.is_empty() { + Hash::Missing + } else if hashes.iter().any(|hash| required_hashes.contains(hash)) { + Hash::Matched + } else { + Hash::Mismatched } - } + } else { + Hash::Matched + }; - SourceDistCompatibility::Compatible + SourceDistCompatibility::Compatible(hash) } fn wheel_compatibility( @@ -174,20 +173,28 @@ impl FlatIndex { return WheelCompatibility::Incompatible(IncompatibleWheel::NoBinary); } - // Check if hashes line up - if let Some(required_hashes) = required_hashes.get(&filename.name) { - if !required_hashes.is_empty() { - if hashes.is_empty() { - return WheelCompatibility::Incompatible(IncompatibleWheel::MissingHash); - } - if !hashes.iter().any(|hash| required_hashes.contains(hash)) { - return WheelCompatibility::Incompatible(IncompatibleWheel::MismatchedHash); - } - } - } - // Determine a compatibility for the wheel based on tags. - WheelCompatibility::from(filename.compatibility(tags)) + let priority = match filename.compatibility(tags) { + TagCompatibility::Incompatible(tag) => { + return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag)) + } + TagCompatibility::Compatible(priority) => priority, + }; + + // Check if hashes line up + let hash = if let Some(required_hashes) = required_hashes.get(&filename.name) { + if hashes.is_empty() { + Hash::Missing + } else if hashes.iter().any(|hash| required_hashes.contains(hash)) { + Hash::Matched + } else { + Hash::Mismatched + } + } else { + Hash::Matched + }; + + WheelCompatibility::Compatible(hash, priority) } /// Get the [`FlatDistributions`] for the given package name. diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 2d714ca38..3b4cad944 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -8,11 +8,11 @@ use tracing::instrument; use distribution_filename::{DistFilename, WheelFilename}; use distribution_types::{ - Dist, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist, + Dist, Hash, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist, SourceDistCompatibility, WheelCompatibility, }; use pep440_rs::{Version, VersionSpecifiers}; -use platform_tags::Tags; +use platform_tags::{TagCompatibility, Tags}; use pypi_types::{HashDigest, Yanked}; use uv_client::{OwnedArchive, SimpleMetadata, VersionFiles}; use uv_configuration::{NoBinary, NoBuild}; @@ -456,19 +456,6 @@ impl VersionMapLazy { )); } - // Check if hashes line up - if !self.required_hashes.is_empty() { - if hashes.is_empty() { - return SourceDistCompatibility::Incompatible(IncompatibleSource::MissingHash); - } - if !hashes - .iter() - .any(|hash| self.required_hashes.contains(hash)) - { - return SourceDistCompatibility::Incompatible(IncompatibleSource::MismatchedHash); - } - } - // Check if yanked if let Some(yanked) = yanked { if yanked.is_yanked() && !self.allowed_yanks.contains(version) { @@ -489,7 +476,23 @@ impl VersionMapLazy { } } - SourceDistCompatibility::Compatible + // Check if hashes line up. If hashes aren't required, they're considered matching. + let hash = if self.required_hashes.is_empty() { + Hash::Matched + } else { + if hashes.is_empty() { + Hash::Missing + } else if hashes + .iter() + .any(|hash| self.required_hashes.contains(hash)) + { + Hash::Matched + } else { + Hash::Mismatched + } + }; + + SourceDistCompatibility::Compatible(hash) } #[allow(clippy::too_many_arguments)] @@ -513,19 +516,6 @@ impl VersionMapLazy { return WheelCompatibility::Incompatible(IncompatibleWheel::ExcludeNewer(upload_time)); } - // Check if hashes line up - if !self.required_hashes.is_empty() { - if hashes.is_empty() { - return WheelCompatibility::Incompatible(IncompatibleWheel::MissingHash); - } - if !hashes - .iter() - .any(|hash| self.required_hashes.contains(hash)) - { - return WheelCompatibility::Incompatible(IncompatibleWheel::MismatchedHash); - } - } - // Check if yanked if let Some(yanked) = yanked { if yanked.is_yanked() && !self.allowed_yanks.contains(version) { @@ -542,8 +532,31 @@ impl VersionMapLazy { } } - // Determine a compatibility for the wheel based on tags - WheelCompatibility::from(filename.compatibility(&self.tags)) + // Determine a compatibility for the wheel based on tags. + let priority = match filename.compatibility(&self.tags) { + TagCompatibility::Incompatible(tag) => { + return WheelCompatibility::Incompatible(IncompatibleWheel::Tag(tag)) + } + TagCompatibility::Compatible(priority) => priority, + }; + + // Check if hashes line up. If hashes aren't required, they're considered matching. + let hash = if self.required_hashes.is_empty() { + Hash::Matched + } else { + if hashes.is_empty() { + Hash::Missing + } else if hashes + .iter() + .any(|hash| self.required_hashes.contains(hash)) + { + Hash::Matched + } else { + Hash::Mismatched + } + }; + + WheelCompatibility::Compatible(hash, priority) } } diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index dec0a147f..b2f0b7b5b 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -3774,12 +3774,11 @@ fn require_hashes_mismatch() -> Result<()> { .arg("requirements.txt") .arg("--require-hashes"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + error: In `--require-hashes` mode, all requirements must be pinned upfront with `==`, but found: idna "### ); diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 387fc25a5..bc07eafc7 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -3177,12 +3177,20 @@ fn require_hashes_wheel_no_binary() -> Result<()> { .arg(":all:") .arg("--require-hashes"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: anyio==4.0.0 + Caused by: Hash mismatch for anyio==4.0.0 + + Expected: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + + Computed: + sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a "### ); @@ -3262,12 +3270,20 @@ fn require_hashes_source_only_binary() -> Result<()> { .arg(":all:") .arg("--require-hashes"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because anyio==4.0.0 is unusable because no wheels are usable and building from source is disabled and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: anyio==4.0.0 + Caused by: Hash mismatch for anyio==4.0.0 + + Expected: + sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + + Computed: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f "### ); @@ -3287,12 +3303,20 @@ fn require_hashes_wrong_digest() -> Result<()> { .arg("requirements.txt") .arg("--require-hashes"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: anyio==4.0.0 + Caused by: Hash mismatch for anyio==4.0.0 + + Expected: + sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + + Computed: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f "### ); @@ -3312,12 +3336,20 @@ fn require_hashes_wrong_algorithm() -> Result<()> { .arg("requirements.txt") .arg("--require-hashes"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: anyio==4.0.0 + Caused by: Hash mismatch for anyio==4.0.0 + + Expected: + sha512:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + + Computed: + sha512:f30761c1e8725b49c498273b90dba4b05c0fd157811994c806183062cb6647e773364ce45f0e1ff0b10e32fe6d0232ea5ad39476ccf37109d6b49603a09c11c2 "### ); @@ -3638,12 +3670,20 @@ fn require_hashes_re_download() -> Result<()> { .arg("--reinstall") .arg("--require-hashes"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because anyio==4.0.0 is unusable because the hash does not match and you require anyio==4.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: anyio==4.0.0 + Caused by: Hash mismatch for anyio==4.0.0 + + Expected: + sha256:afdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + + Computed: + sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f "### ); @@ -3661,8 +3701,6 @@ fn require_hashes_re_download() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - Downloaded 1 package in [TIME] Uninstalled 1 package in [TIME] Installed 1 package in [TIME] - anyio==4.0.0 @@ -4095,22 +4133,111 @@ fn require_hashes_at_least_one() -> Result<()> { fn require_hashes_find_links_no_hash() -> Result<()> { let context = TestContext::new("3.12"); + // First, use the correct hash. let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt .write_str("example-a-961b4c22==1.0.0 --hash=sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e")?; uv_snapshot!(command(&context) .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/no-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + example-a-961b4c22==1.0.0 + "### + ); + + // Second, use an incorrect hash. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str("example-a-961b4c22==1.0.0 --hash=sha256:123")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") .arg("--require-hashes") .arg("--find-links") .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/no-hash/simple-html/example-a-961b4c22/index.html"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because it has no hash and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + Caused by: Hash mismatch for example-a-961b4c22==1.0.0 + + Expected: + sha256:123 + + Computed: + sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e + "### + ); + + // Third, use the hash from the source distribution. This will actually fail, when it _could_ + // succeed, but pip has the same behavior. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:294e788dbe500fdc39e8b88e82652ab67409a1dc9dd06543d0fe0ae31b713eb3")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/no-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + Caused by: Hash mismatch for example-a-961b4c22==1.0.0 + + Expected: + sha256:294e788dbe500fdc39e8b88e82652ab67409a1dc9dd06543d0fe0ae31b713eb3 + + Computed: + sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e + "### + ); + + // Fourth, use the hash from the source distribution, and disable wheels. This should succeed. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str("example-a-961b4c22==1.0.0 --hash=sha256:294e788dbe500fdc39e8b88e82652ab67409a1dc9dd06543d0fe0ae31b713eb3")?; + + uv_snapshot!(command(&context) + .arg("requirements.txt") + .arg("--no-binary") + .arg(":all:") + .arg("--reinstall") + .arg("--require-hashes") + .arg("--find-links") + .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/no-hash/simple-html/example-a-961b4c22/index.html"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - example-a-961b4c22==1.0.0 + + example-a-961b4c22==1.0.0 "### ); @@ -4162,12 +4289,20 @@ fn require_hashes_find_links_invalid_hash() -> Result<()> { .arg("--find-links") .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/invalid-hash/simple-html/example-a-961b4c22/index.html"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because the hash does not match and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + Caused by: Hash mismatch for example-a-961b4c22==1.0.0 + + Expected: + sha256:123 + + Computed: + sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e "### ); @@ -4235,13 +4370,17 @@ fn require_hashes_find_links_invalid_hash() -> Result<()> { .arg("--require-hashes") .arg("--find-links") .arg("https://raw.githubusercontent.com/astral-test/astral-test-hash/main/invalid-hash/simple-html/example-a-961b4c22/index.html"), @r###" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because the hash does not match and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - example-a-961b4c22==1.0.0 + + example-a-961b4c22==1.0.0 "### ); @@ -4263,9 +4402,7 @@ fn require_hashes_find_links_invalid_hash() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - error: Failed to download distributions - Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + error: Failed to download and build: example-a-961b4c22==1.0.0 Caused by: Hash mismatch for example-a-961b4c22==1.0.0 Expected: @@ -4294,13 +4431,15 @@ fn require_hashes_registry_no_hash() -> Result<()> { .arg("--require-hashes") .arg("--index-url") .arg("https://astral-test.github.io/astral-test-hash/no-hash/simple-html/"), @r###" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because it has no hash and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + example-a-961b4c22==1.0.0 "### ); @@ -4350,12 +4489,20 @@ fn require_hashes_registry_invalid_hash() -> Result<()> { .arg("--index-url") .arg("https://astral-test.github.io/astral-test-hash/invalid-hash/simple-html/"), @r###" success: false - exit_code: 1 + exit_code: 2 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because the hash does not match and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + error: Failed to download distributions + Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + Caused by: Hash mismatch for example-a-961b4c22==1.0.0 + + Expected: + sha256:123 + + Computed: + sha256:5d69f0b590514103234f0c3526563856f04d044d8d0ea1073a843ae429b3187e "### ); @@ -4423,13 +4570,17 @@ fn require_hashes_registry_invalid_hash() -> Result<()> { .arg("--require-hashes") .arg("--index-url") .arg("https://astral-test.github.io/astral-test-hash/invalid-hash/simple-html/"), @r###" - success: false - exit_code: 1 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - × No solution found when resolving dependencies: - ╰─▶ Because example-a-961b4c22==1.0.0 is unusable because the hash does not match and you require example-a-961b4c22==1.0.0, we can conclude that the requirements are unsatisfiable. + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - example-a-961b4c22==1.0.0 + + example-a-961b4c22==1.0.0 "### ); @@ -4451,9 +4602,7 @@ fn require_hashes_registry_invalid_hash() -> Result<()> { ----- stdout ----- ----- stderr ----- - Resolved 1 package in [TIME] - error: Failed to download distributions - Caused by: Failed to fetch wheel: example-a-961b4c22==1.0.0 + error: Failed to download and build: example-a-961b4c22==1.0.0 Caused by: Hash mismatch for example-a-961b4c22==1.0.0 Expected: From 8513d603b44da41c62e4d0f5322aa0d4f3bd6f40 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 15:31:41 -0400 Subject: [PATCH 093/110] Return computed hashes from metadata requests (#2951) ## Summary This PR modifies the distribution database to return both the `Metadata23` and the computed hashes when clients request metadata. No behavior changes, but this will be necessary to power `--generate-hashes`. --- .../src/distribution_database.rs | 14 ++--- crates/uv-distribution/src/lib.rs | 19 +++++++ crates/uv-distribution/src/source/mod.rs | 54 ++++++++++++------- crates/uv-requirements/src/lookahead.rs | 14 ++--- crates/uv-requirements/src/source_tree.rs | 14 ++--- crates/uv-requirements/src/unnamed.rs | 14 ++--- crates/uv-resolver/src/resolution.rs | 12 ++--- crates/uv-resolver/src/resolver/mod.rs | 13 ++--- crates/uv-resolver/src/resolver/provider.rs | 6 +-- 9 files changed, 100 insertions(+), 60 deletions(-) diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 0db62a92d..6f6566c24 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -25,7 +25,7 @@ use uv_types::BuildContext; use crate::archive::Archive; use crate::locks::Locks; -use crate::{Error, LocalWheel, Reporter, SourceDistributionBuilder}; +use crate::{ArchiveMetadata, Error, LocalWheel, Reporter, SourceDistributionBuilder}; /// A cached high-level interface to convert distributions (a requirement resolved to a location) /// to a wheel or wheel metadata. @@ -109,7 +109,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> &self, dist: &Dist, hashes: &[HashDigest], - ) -> Result { + ) -> Result { match dist { Dist::Built(built) => self.get_wheel_metadata(built, hashes).await, Dist::Source(source) => { @@ -343,16 +343,18 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> &self, dist: &BuiltDist, hashes: &[HashDigest], - ) -> Result { + ) -> Result { match self.client.wheel_metadata(dist).boxed().await { - Ok(metadata) => Ok(metadata), + Ok(metadata) => Ok(ArchiveMetadata::from(metadata)), Err(err) if err.is_http_streaming_unsupported() => { warn!("Streaming unsupported when fetching metadata for {dist}; downloading wheel directly ({err})"); // If the request failed due to an error that could be resolved by // downloading the wheel directly, try that. let wheel = self.get_wheel(dist, hashes).await?; - Ok(wheel.metadata()?) + let metadata = wheel.metadata()?; + let hashes = wheel.hashes; + Ok(ArchiveMetadata { metadata, hashes }) } Err(err) => Err(err.into()), } @@ -366,7 +368,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> &self, source: &BuildableSource<'_>, hashes: &[HashDigest], - ) -> Result { + ) -> Result { let no_build = match self.build_context.no_build() { NoBuild::All => true, NoBuild::None => false, diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index 61eeb41a4..5245cc265 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -4,6 +4,7 @@ pub use download::LocalWheel; pub use error::Error; pub use git::{is_same_reference, to_precise}; pub use index::{BuiltWheelIndex, RegistryWheelIndex}; +use pypi_types::{HashDigest, Metadata23}; pub use reporter::Reporter; pub use source::SourceDistributionBuilder; @@ -16,3 +17,21 @@ mod index; mod locks; mod reporter; mod source; + +/// The metadata associated with an archive. +#[derive(Debug, Clone)] +pub struct ArchiveMetadata { + /// The [`Metadata23`] for the underlying distribution. + pub metadata: Metadata23, + /// The hashes of the source or built archive. + pub hashes: Vec, +} + +impl From for ArchiveMetadata { + fn from(metadata: Metadata23) -> Self { + Self { + metadata, + hashes: vec![], + } + } +} diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index deb37e753..b64f0025f 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -37,7 +37,7 @@ use crate::error::Error; use crate::git::{fetch_git_archive, resolve_precise}; use crate::source::built_wheel_metadata::BuiltWheelMetadata; use crate::source::revision::Revision; -use crate::Reporter; +use crate::{ArchiveMetadata, Reporter}; mod built_wheel_metadata; mod revision; @@ -215,7 +215,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, hashes: &[HashDigest], - ) -> Result { + ) -> Result { let metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { let url = match &dist.file.url { @@ -419,7 +419,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { cache_shard: &CacheShard, subdirectory: Option<&'data Path>, hashes: &[HashDigest], - ) -> Result { + ) -> Result { // Fetch the revision for the source distribution. let revision = self .url_revision(source, filename, url, cache_shard, hashes) @@ -442,7 +442,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let metadata_entry = cache_shard.entry(METADATA); if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { debug!("Using cached metadata for: {source}"); - return Ok(metadata); + return Ok(ArchiveMetadata { + metadata, + hashes: revision.into_hashes(), + }); } // Otherwise, we either need to build the metadata or the wheel. @@ -463,7 +466,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - return Ok(metadata); + return Ok(ArchiveMetadata { + metadata, + hashes: revision.into_hashes(), + }); } let task = self @@ -488,7 +494,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } } - Ok(metadata) + Ok(ArchiveMetadata { + metadata, + hashes: revision.into_hashes(), + }) } /// Return the [`Revision`] for a remote URL, refreshing it if necessary. @@ -632,7 +641,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, hashes: &[HashDigest], - ) -> Result { + ) -> Result { let cache_shard = self.build_context.cache().shard( CacheBucket::BuiltWheels, WheelCache::Path(resource.url).root(), @@ -660,7 +669,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let metadata_entry = cache_shard.entry(METADATA); if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { debug!("Using cached metadata for: {source}"); - return Ok(metadata); + return Ok(ArchiveMetadata { + metadata, + hashes: revision.into_hashes(), + }); } let source_entry = cache_shard.entry("source"); @@ -680,7 +692,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - return Ok(metadata); + return Ok(ArchiveMetadata { + metadata, + hashes: revision.into_hashes(), + }); } // Otherwise, we need to build a wheel. @@ -705,7 +720,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - Ok(metadata) + Ok(ArchiveMetadata { + metadata, + hashes: revision.into_hashes(), + }) } /// Return the [`Revision`] for a local archive, refreshing it if necessary. @@ -826,7 +844,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, hashes: &[HashDigest], - ) -> Result { + ) -> Result { // Before running the build, check that the hashes match. if !hashes.is_empty() { return Err(Error::HashesNotSupportedSourceTree(source.to_string())); @@ -850,7 +868,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let metadata_entry = cache_shard.entry(METADATA); if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { debug!("Using cached metadata for: {source}"); - return Ok(metadata); + return Ok(ArchiveMetadata::from(metadata)); } // If the backend supports `prepare_metadata_for_build_wheel`, use it. @@ -868,7 +886,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - return Ok(metadata); + return Ok(ArchiveMetadata::from(metadata)); } // Otherwise, we need to build a wheel. @@ -893,7 +911,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - Ok(metadata) + Ok(ArchiveMetadata::from(metadata)) } /// Return the [`Revision`] for a local source tree, refreshing it if necessary. @@ -1000,7 +1018,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &GitSourceUrl<'_>, hashes: &[HashDigest], - ) -> Result { + ) -> Result { // Before running the build, check that the hashes match. if !hashes.is_empty() { return Err(Error::HashesNotSupportedGit(source.to_string())); @@ -1039,7 +1057,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { { if let Some(metadata) = read_cached_metadata(&metadata_entry).await? { debug!("Using cached metadata for: {source}"); - return Ok(metadata); + return Ok(ArchiveMetadata::from(metadata)); } } @@ -1058,7 +1076,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - return Ok(metadata); + return Ok(ArchiveMetadata::from(metadata)); } // Otherwise, we need to build a wheel. @@ -1083,7 +1101,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await .map_err(Error::CacheWrite)?; - Ok(metadata) + Ok(ArchiveMetadata::from(metadata)) } /// Download and unzip a source distribution into the cache from an HTTP response. diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index e38521bac..31168bdf9 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -139,24 +139,24 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { // Fetch the metadata for the distribution. let requires_dist = { let id = dist.package_id(); - if let Some(metadata) = self + if let Some(archive) = self .index .get_metadata(&id) .as_deref() .and_then(|response| { - if let MetadataResponse::Found(metadata) = response { - Some(metadata) + if let MetadataResponse::Found(archive, ..) = response { + Some(archive) } else { None } }) { // If the metadata is already in the index, return it. - metadata.requires_dist.clone() + archive.metadata.requires_dist.clone() } else { // Run the PEP 517 build process to extract metadata from the source distribution. let hashes = self.hashes.get(dist.name()).unwrap_or_default(); - let metadata = self + let archive = self .database .get_or_build_wheel_metadata(&dist, hashes) .await @@ -165,11 +165,11 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { Dist::Source(source) => format!("Failed to download and build: {source}"), })?; - let requires_dist = metadata.requires_dist.clone(); + let requires_dist = archive.metadata.requires_dist.clone(); // Insert the metadata into the index. self.index - .insert_metadata(id, MetadataResponse::Found(metadata)); + .insert_metadata(id, MetadataResponse::Found(archive)); requires_dist } diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index b6cccd01b..b62dcc1e2 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -100,30 +100,30 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { // Fetch the metadata for the distribution. let metadata = { let id = PackageId::from_url(source.url()); - if let Some(metadata) = self + if let Some(archive) = self .index .get_metadata(&id) .as_deref() .and_then(|response| { - if let MetadataResponse::Found(metadata) = response { - Some(metadata) + if let MetadataResponse::Found(archive) = response { + Some(archive) } else { None } }) { // If the metadata is already in the index, return it. - metadata.clone() + archive.metadata.clone() } else { // Run the PEP 517 build process to extract metadata from the source distribution. let source = BuildableSource::Url(source); - let metadata = self.database.build_wheel_metadata(&source, &[]).await?; + let archive = self.database.build_wheel_metadata(&source, &[]).await?; // Insert the metadata into the index. self.index - .insert_metadata(id, MetadataResponse::Found(metadata.clone())); + .insert_metadata(id, MetadataResponse::Found(archive.clone())); - metadata + archive.metadata } }; diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index e658852ae..5c4d4b3b5 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -250,24 +250,24 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont // Fetch the metadata for the distribution. let name = { let id = PackageId::from_url(source.url()); - if let Some(metadata) = index.get_metadata(&id).as_deref().and_then(|response| { - if let MetadataResponse::Found(metadata) = response { - Some(metadata) + if let Some(archive) = index.get_metadata(&id).as_deref().and_then(|response| { + if let MetadataResponse::Found(archive) = response { + Some(archive) } else { None } }) { // If the metadata is already in the index, return it. - metadata.name.clone() + archive.metadata.name.clone() } else { // Run the PEP 517 build process to extract metadata from the source distribution. let source = BuildableSource::Url(source); - let metadata = database.build_wheel_metadata(&source, &[]).await?; + let archive = database.build_wheel_metadata(&source, &[]).await?; - let name = metadata.name.clone(); + let name = archive.metadata.name.clone(); // Insert the metadata into the index. - index.insert_metadata(id, MetadataResponse::Found(metadata)); + index.insert_metadata(id, MetadataResponse::Found(archive)); name } diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index 9dbf758dc..2b69d5a4a 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -177,14 +177,14 @@ impl ResolutionGraph { ) }); - let MetadataResponse::Found(metadata) = &*response else { + let MetadataResponse::Found(archive) = &*response else { panic!( "Every package should have metadata: {:?}", dist.package_id() ) }; - if metadata.provides_extras.contains(extra) { + if archive.metadata.provides_extras.contains(extra) { extras .entry(package_name.clone()) .or_insert_with(Vec::new) @@ -231,14 +231,14 @@ impl ResolutionGraph { ) }); - let MetadataResponse::Found(metadata) = &*response else { + let MetadataResponse::Found(archive) = &*response else { panic!( "Every package should have metadata: {:?}", dist.package_id() ) }; - if metadata.provides_extras.contains(extra) { + if archive.metadata.provides_extras.contains(extra) { extras .entry(package_name.clone()) .or_insert_with(Vec::new) @@ -441,13 +441,13 @@ impl ResolutionGraph { .distributions .get(&package_id) .expect("every package in resolution graph has metadata"); - let MetadataResponse::Found(md) = &*res else { + let MetadataResponse::Found(archive, ..) = &*res else { panic!( "Every package should have metadata: {:?}", dist.package_id() ) }; - for req in manifest.apply(&md.requires_dist) { + for req in manifest.apply(&archive.metadata.requires_dist) { let Some(ref marker_tree) = req.marker else { continue; }; diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 732cdf867..4e1c89547 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -28,7 +28,7 @@ use pypi_types::Metadata23; pub(crate) use urls::Urls; use uv_client::RegistryClient; use uv_configuration::{Constraints, Overrides}; -use uv_distribution::DistributionDatabase; +use uv_distribution::{ArchiveMetadata, DistributionDatabase}; use uv_interpreter::Interpreter; use uv_normalize::PackageName; use uv_types::{BuildContext, InstalledPackagesProvider, RequiredHashes}; @@ -659,7 +659,7 @@ impl< // If we failed to fetch the metadata for a URL, we can't proceed. let metadata = match &*response { - MetadataResponse::Found(metadata) => metadata, + MetadataResponse::Found(archive) => &archive.metadata, MetadataResponse::Offline => { self.unavailable_packages .insert(package_name.clone(), UnavailablePackage::Offline); @@ -966,7 +966,7 @@ impl< .ok_or(ResolveError::Unregistered)?; let metadata = match &*response { - MetadataResponse::Found(metadata) => metadata, + MetadataResponse::Found(archive) => &archive.metadata, MetadataResponse::Offline => { self.incomplete_packages .entry(package_name.clone()) @@ -1067,9 +1067,10 @@ impl< } Some(Response::Installed { dist, metadata }) => { trace!("Received installed distribution metadata for: {dist}"); - self.index - .distributions - .done(dist.package_id(), MetadataResponse::Found(metadata)); + self.index.distributions.done( + dist.package_id(), + MetadataResponse::Found(ArchiveMetadata::from(metadata)), + ); } Some(Response::Dist { dist: Dist::Built(dist), diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index eb36edb03..7c26b9e69 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -5,10 +5,10 @@ use chrono::{DateTime, Utc}; use distribution_types::{Dist, IndexLocations, Name}; use platform_tags::Tags; -use pypi_types::Metadata23; + use uv_client::RegistryClient; use uv_configuration::{NoBinary, NoBuild}; -use uv_distribution::DistributionDatabase; +use uv_distribution::{ArchiveMetadata, DistributionDatabase}; use uv_normalize::PackageName; use uv_types::{BuildContext, RequiredHashes}; @@ -36,7 +36,7 @@ pub enum VersionsResponse { #[derive(Debug)] pub enum MetadataResponse { /// The wheel metadata was found and parsed successfully. - Found(Metadata23), + Found(ArchiveMetadata), /// The wheel metadata was found, but could not be parsed. InvalidMetadata(Box), /// The wheel metadata was found, but the metadata was inconsistent. From 006379c50cd7217d47a0d163819c809a69f36893 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 16:02:45 -0400 Subject: [PATCH 094/110] Add support for URL requirements in `--generate-hashes` (#2952) ## Summary This PR enables hash generation for URL requirements when the user provides `--generate-hashes` to `pip compile`. While we include the hashes from the registry already, today, we omit hashes for URLs. To power hash generation, we introduce a `HashPolicy` abstraction: ```rust #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum HashPolicy<'a> { /// No hash policy is specified. None, /// Hashes should be generated (specifically, a SHA-256 hash), but not validated. Generate, /// Hashes should be validated against a pre-defined list of hashes. If necessary, hashes should /// be generated so as to ensure that the archive is valid. Validate(&'a [HashDigest]), } ``` All of the methods on the distribution database now accept this policy, instead of accepting `&'a [HashDigest]`. Closes #2378. --- crates/distribution-types/src/cached.rs | 2 +- crates/distribution-types/src/hash.rs | 84 ++++++ crates/distribution-types/src/hashed.rs | 27 -- crates/distribution-types/src/lib.rs | 4 +- crates/uv-dev/src/resolve_cli.rs | 7 +- crates/uv-dispatch/src/lib.rs | 13 +- .../src/distribution_database.rs | 64 ++--- .../src/index/built_wheel_index.rs | 22 +- .../src/index/registry_wheel_index.rs | 19 +- crates/uv-distribution/src/source/mod.rs | 66 ++--- crates/uv-installer/src/downloader.rs | 14 +- crates/uv-installer/src/plan.rs | 14 +- crates/uv-requirements/src/lookahead.rs | 11 +- crates/uv-requirements/src/source_tree.rs | 36 +-- crates/uv-requirements/src/unnamed.rs | 41 +-- crates/uv-resolver/src/flat_index.rs | 42 ++- crates/uv-resolver/src/hash_checking_mode.rs | 15 - crates/uv-resolver/src/lib.rs | 2 - crates/uv-resolver/src/options.rs | 11 - crates/uv-resolver/src/resolution.rs | 16 +- crates/uv-resolver/src/resolver/mod.rs | 29 +- crates/uv-resolver/src/resolver/provider.rs | 17 +- crates/uv-resolver/src/version_map.rs | 9 +- crates/uv-resolver/tests/resolver.rs | 4 +- crates/uv-types/src/{hashes.rs => hash.rs} | 75 +++-- crates/uv-types/src/lib.rs | 4 +- crates/uv/src/commands/pip_compile.rs | 22 +- crates/uv/src/commands/pip_install.rs | 48 ++-- crates/uv/src/commands/pip_sync.rs | 48 ++-- crates/uv/src/commands/venv.rs | 4 +- crates/uv/tests/pip_compile.rs | 271 +++++++++++------- 31 files changed, 546 insertions(+), 495 deletions(-) create mode 100644 crates/distribution-types/src/hash.rs delete mode 100644 crates/distribution-types/src/hashed.rs delete mode 100644 crates/uv-resolver/src/hash_checking_mode.rs rename crates/uv-types/src/{hashes.rs => hash.rs} (57%) diff --git a/crates/distribution-types/src/cached.rs b/crates/distribution-types/src/cached.rs index 9661d3cfe..b7ddf5155 100644 --- a/crates/distribution-types/src/cached.rs +++ b/crates/distribution-types/src/cached.rs @@ -8,7 +8,7 @@ use pypi_types::HashDigest; use uv_normalize::PackageName; use crate::direct_url::{DirectUrl, LocalFileUrl}; -use crate::hashed::Hashed; +use crate::hash::Hashed; use crate::{ BuiltDist, Dist, DistributionMetadata, InstalledMetadata, InstalledVersion, Name, SourceDist, VersionOrUrl, diff --git a/crates/distribution-types/src/hash.rs b/crates/distribution-types/src/hash.rs new file mode 100644 index 000000000..553a74f55 --- /dev/null +++ b/crates/distribution-types/src/hash.rs @@ -0,0 +1,84 @@ +use pypi_types::{HashAlgorithm, HashDigest}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum HashPolicy<'a> { + /// No hash policy is specified. + None, + /// Hashes should be generated (specifically, a SHA-256 hash), but not validated. + Generate, + /// Hashes should be validated against a pre-defined list of hashes. If necessary, hashes should + /// be generated so as to ensure that the archive is valid. + Validate(&'a [HashDigest]), +} + +impl<'a> HashPolicy<'a> { + /// Returns `true` if the hash policy is `None`. + pub fn is_none(&self) -> bool { + matches!(self, Self::None) + } + + /// Returns `true` if the hash policy is `Generate`. + pub fn is_generate(&self) -> bool { + matches!(self, Self::Generate) + } + + /// Returns `true` if the hash policy is `Validate`. + pub fn is_validate(&self) -> bool { + matches!(self, Self::Validate(_)) + } + + /// Return the algorithms used in the hash policy. + pub fn algorithms(&self) -> Vec { + match self { + Self::None => vec![], + Self::Generate => vec![HashAlgorithm::Sha256], + Self::Validate(hashes) => { + let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::>(); + algorithms.sort(); + algorithms.dedup(); + algorithms + } + } + } + + /// Return the digests used in the hash policy. + pub fn digests(&self) -> &[HashDigest] { + match self { + Self::None => &[], + Self::Generate => &[], + Self::Validate(hashes) => hashes, + } + } +} + +pub trait Hashed { + /// Return the [`HashDigest`]s for the archive. + fn hashes(&self) -> &[HashDigest]; + + /// Returns `true` if the archive satisfies the given hash policy. + fn satisfies(&self, hashes: HashPolicy) -> bool { + match hashes { + HashPolicy::None => true, + HashPolicy::Generate => self + .hashes() + .iter() + .any(|hash| hash.algorithm == HashAlgorithm::Sha256), + HashPolicy::Validate(hashes) => self.hashes().iter().any(|hash| hashes.contains(hash)), + } + } + + /// Returns `true` if the archive includes a hash for at least one of the given algorithms. + fn has_digests(&self, hashes: HashPolicy) -> bool { + match hashes { + HashPolicy::None => true, + HashPolicy::Generate => self + .hashes() + .iter() + .any(|hash| hash.algorithm == HashAlgorithm::Sha256), + HashPolicy::Validate(hashes) => hashes + .iter() + .map(HashDigest::algorithm) + .any(|algorithm| self.hashes().iter().any(|hash| hash.algorithm == algorithm)), + } + } +} diff --git a/crates/distribution-types/src/hashed.rs b/crates/distribution-types/src/hashed.rs deleted file mode 100644 index c8185021c..000000000 --- a/crates/distribution-types/src/hashed.rs +++ /dev/null @@ -1,27 +0,0 @@ -use pypi_types::HashDigest; - -pub trait Hashed { - /// Return the [`HashDigest`]s for the archive. - fn hashes(&self) -> &[HashDigest]; - - /// Returns `true` if the archive satisfies the given hashes. - fn satisfies(&self, hashes: &[HashDigest]) -> bool { - if hashes.is_empty() { - true - } else { - self.hashes().iter().any(|hash| hashes.contains(hash)) - } - } - - /// Returns `true` if the archive includes a hash for at least one of the given algorithms. - fn has_digests(&self, hashes: &[HashDigest]) -> bool { - if hashes.is_empty() { - true - } else { - hashes - .iter() - .map(HashDigest::algorithm) - .any(|algorithm| self.hashes().iter().any(|hash| hash.algorithm == algorithm)) - } - } -} diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index ab74423ee..f4ebe5c3c 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -51,7 +51,7 @@ pub use crate::direct_url::*; pub use crate::editable::*; pub use crate::error::*; pub use crate::file::*; -pub use crate::hashed::*; +pub use crate::hash::*; pub use crate::id::*; pub use crate::index_url::*; pub use crate::installed::*; @@ -67,7 +67,7 @@ mod direct_url; mod editable; mod error; mod file; -mod hashed; +mod hash; mod id; mod index_url; mod installed; diff --git a/crates/uv-dev/src/resolve_cli.rs b/crates/uv-dev/src/resolve_cli.rs index df3d62187..b4aa0d992 100644 --- a/crates/uv-dev/src/resolve_cli.rs +++ b/crates/uv-dev/src/resolve_cli.rs @@ -18,7 +18,7 @@ use uv_dispatch::BuildDispatch; use uv_installer::SitePackages; use uv_interpreter::PythonEnvironment; use uv_resolver::{FlatIndex, InMemoryIndex, Manifest, Options, Resolver}; -use uv_types::{BuildIsolation, InFlight, RequiredHashes}; +use uv_types::{BuildIsolation, HashStrategy, InFlight}; #[derive(ValueEnum, Default, Clone)] pub(crate) enum ResolveCliFormat { @@ -58,7 +58,6 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { let index_locations = IndexLocations::new(args.index_url, args.extra_index_url, args.find_links, false); let index = InMemoryIndex::default(); - let hashes = RequiredHashes::default(); let in_flight = InFlight::default(); let no_build = if args.no_build { NoBuild::All @@ -74,7 +73,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { FlatIndex::from_entries( entries, venv.interpreter().tags()?, - &RequiredHashes::default(), + &HashStrategy::None, &no_build, &NoBinary::None, ) @@ -109,7 +108,7 @@ pub(crate) async fn resolve_cli(args: ResolveCliArgs) -> Result<()> { &client, &flat_index, &index, - &hashes, + &HashStrategy::None, &build_dispatch, &site_packages, )?; diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index fa8ea31b4..dab5b2cbe 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -21,7 +21,7 @@ use uv_configuration::{BuildKind, ConfigSettings, NoBinary, NoBuild, Reinstall, use uv_installer::{Downloader, Installer, Plan, Planner, SitePackages}; use uv_interpreter::{Interpreter, PythonEnvironment}; use uv_resolver::{FlatIndex, InMemoryIndex, Manifest, Options, Resolver}; -use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, InFlight, RequiredHashes}; +use uv_types::{BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; /// The main implementation of [`BuildContext`], used by the CLI, see [`BuildContext`] /// documentation. @@ -134,7 +134,6 @@ impl<'a> BuildContext for BuildDispatch<'a> { async fn resolve<'data>(&'data self, requirements: &'data [Requirement]) -> Result { let markers = self.interpreter.markers(); let tags = self.interpreter.tags()?; - let hashes = RequiredHashes::default(); let resolver = Resolver::new( Manifest::simple(requirements.to_vec()), self.options, @@ -144,7 +143,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.client, self.flat_index, self.index, - &hashes, + &HashStrategy::None, self, &EmptyInstalledPackages, )?; @@ -178,9 +177,6 @@ impl<'a> BuildContext for BuildDispatch<'a> { venv.root().display(), ); - // Don't enforce hashes for build dependencies. - let hashes = RequiredHashes::default(); - // Determine the current environment markers. let tags = self.interpreter.tags()?; @@ -197,7 +193,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { site_packages, &Reinstall::None, &NoBinary::None, - &RequiredHashes::default(), + &HashStrategy::None, self.index_locations, self.cache(), venv, @@ -226,7 +222,8 @@ impl<'a> BuildContext for BuildDispatch<'a> { vec![] } else { // TODO(konstin): Check that there is no endless recursion. - let downloader = Downloader::new(self.cache, tags, &hashes, self.client, self); + let downloader = + Downloader::new(self.cache, tags, &HashStrategy::None, self.client, self); debug!( "Downloading and building requirement{} for build: {}", if remote.len() == 1 { "" } else { "s" }, diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 6f6566c24..1f98abee8 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -11,8 +11,8 @@ use url::Url; use distribution_filename::WheelFilename; use distribution_types::{ - BuildableSource, BuiltDist, Dist, FileLocation, Hashed, IndexLocations, LocalEditable, Name, - SourceDist, + BuildableSource, BuiltDist, Dist, FileLocation, HashPolicy, Hashed, IndexLocations, + LocalEditable, Name, SourceDist, }; use platform_tags::Tags; use pypi_types::{HashDigest, Metadata23}; @@ -91,7 +91,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> &self, dist: &Dist, tags: &Tags, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { match dist { Dist::Built(built) => self.get_wheel(built, hashes).await, @@ -108,7 +108,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> pub async fn get_or_build_wheel_metadata( &self, dist: &Dist, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { match dist { Dist::Built(built) => self.get_wheel_metadata(built, hashes).await, @@ -147,12 +147,12 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> /// Fetch a wheel from the cache or download it from the index. /// - /// While hashes will be generated in some cases, hash-checking is _not_ enforced and should + /// While hashes will be generated in all cases, hash-checking is _not_ enforced and should /// instead be enforced by the caller. async fn get_wheel( &self, dist: &BuiltDist, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { let no_binary = match self.build_context.no_binary() { NoBinary::None => false, @@ -298,7 +298,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> &self, dist: &SourceDist, tags: &Tags, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { let lock = self.locks.acquire(&Dist::Source(dist.clone())).await; let _guard = lock.lock().await; @@ -342,8 +342,21 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> pub async fn get_wheel_metadata( &self, dist: &BuiltDist, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { + // If hash generation is enabled, and the distribution isn't hosted on an index, get the + // entire wheel to ensure that the hashes are included in the response. If the distribution + // is hosted on an index, the hashes will be included in the simple metadata response. + // For hash _validation_, callers are expected to enforce the policy when retrieving the + // wheel. + // TODO(charlie): Request the hashes via a separate method, to reduce the coupling in this API. + if hashes.is_generate() && matches!(dist, BuiltDist::DirectUrl(_) | BuiltDist::Path(_)) { + let wheel = self.get_wheel(dist, hashes).await?; + let metadata = wheel.metadata()?; + let hashes = wheel.hashes; + return Ok(ArchiveMetadata { metadata, hashes }); + } + match self.client.wheel_metadata(dist).boxed().await { Ok(metadata) => Ok(ArchiveMetadata::from(metadata)), Err(err) if err.is_http_streaming_unsupported() => { @@ -367,7 +380,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> pub async fn build_wheel_metadata( &self, source: &BuildableSource<'_>, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { let no_build = match self.build_context.no_build() { NoBuild::All => true, @@ -400,7 +413,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> filename: &WheelFilename, wheel_entry: &CacheEntry, dist: &BuiltDist, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.stem())); @@ -413,12 +426,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .into_async_read(); // Create a hasher for each hash algorithm. - let algorithms = { - let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); - hash.sort(); - hash.dedup(); - hash - }; + let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers); @@ -428,7 +436,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> uv_extract::stream::unzip(&mut hasher, temp_dir.path()).await?; // If necessary, exhaust the reader to compute the hash. - if !hashes.is_empty() { + if !hashes.is_none() { hasher.finish().await.map_err(Error::HashExhaustion)?; } @@ -492,7 +500,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> filename: &WheelFilename, wheel_entry: &CacheEntry, dist: &BuiltDist, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Create an entry for the HTTP cache. let http_entry = wheel_entry.with_file(format!("{}.http", filename.stem())); @@ -521,7 +529,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .map_err(Error::CacheWrite)?; // If no hashes are required, parallelize the unzip operation. - let hashes = if hashes.is_empty() { + let hashes = if hashes.is_none() { let file = file.into_std().await; tokio::task::spawn_blocking({ let target = temp_dir.path().to_owned(); @@ -536,12 +544,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> vec![] } else { // Create a hasher for each hash algorithm. - let algorithms = { - let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); - hash.sort(); - hash.dedup(); - hash - }; + let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers); uv_extract::stream::unzip(&mut hasher, temp_dir.path()).await?; @@ -609,7 +612,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> filename: &WheelFilename, wheel_entry: CacheEntry, dist: &BuiltDist, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Determine the last-modified time of the wheel. let modified = ArchiveTimestamp::from_file(path).map_err(Error::CacheRead)?; @@ -626,7 +629,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> hashes: archive.hashes, filename: filename.clone(), }) - } else if hashes.is_empty() { + } else if hashes.is_none() { // Otherwise, unzip the wheel. let archive = Archive::new(self.unzip_wheel(path, wheel_entry.path()).await?, vec![]); write_timestamped_archive(&archive_entry, archive.clone(), modified).await?; @@ -646,12 +649,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .map_err(Error::CacheWrite)?; // Create a hasher for each hash algorithm. - let algorithms = { - let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); - hash.sort(); - hash.dedup(); - hash - }; + let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(file, &mut hashers); diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index ea7fd1df6..c856bfdc2 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -4,7 +4,7 @@ use distribution_types::{ use platform_tags::Tags; use uv_cache::{ArchiveTimestamp, Cache, CacheBucket, CacheShard, WheelCache}; use uv_fs::symlinks; -use uv_types::RequiredHashes; +use uv_types::HashStrategy; use crate::index::cached_wheel::CachedWheel; use crate::source::{read_http_revision, read_timestamped_revision, REVISION}; @@ -15,16 +15,16 @@ use crate::Error; pub struct BuiltWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, - hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, } impl<'a> BuiltWheelIndex<'a> { /// Initialize an index of built distributions. - pub fn new(cache: &'a Cache, tags: &'a Tags, hashes: &'a RequiredHashes) -> Self { + pub fn new(cache: &'a Cache, tags: &'a Tags, hasher: &'a HashStrategy) -> Self { Self { cache, tags, - hashes, + hasher, } } @@ -46,10 +46,8 @@ impl<'a> BuiltWheelIndex<'a> { }; // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. - if let Some(hashes) = self.hashes.get(&source_dist.name) { - if !revision.satisfies(hashes) { - return Ok(None); - } + if !revision.satisfies(self.hasher.get(&source_dist.name)) { + return Ok(None); } Ok(self.find(&cache_shard.shard(revision.id()))) @@ -76,10 +74,8 @@ impl<'a> BuiltWheelIndex<'a> { }; // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. - if let Some(hashes) = self.hashes.get(&source_dist.name) { - if !revision.satisfies(hashes) { - return Ok(None); - } + if !revision.satisfies(self.hasher.get(&source_dist.name)) { + return Ok(None); } Ok(self.find(&cache_shard.shard(revision.id()))) @@ -88,7 +84,7 @@ impl<'a> BuiltWheelIndex<'a> { /// Return the most compatible [`CachedWheel`] for a given source distribution at a git URL. pub fn git(&self, source_dist: &GitSourceDist) -> Option { // Enforce hash-checking, which isn't supported for Git distributions. - if self.hashes.get(&source_dist.name).is_some() { + if self.hasher.get(&source_dist.name).is_validate() { return None; } diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 34e5a85c3..da184b20f 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -10,7 +10,7 @@ use platform_tags::Tags; use uv_cache::{Cache, CacheBucket, WheelCache}; use uv_fs::{directories, files, symlinks}; use uv_normalize::PackageName; -use uv_types::RequiredHashes; +use uv_types::HashStrategy; use crate::index::cached_wheel::CachedWheel; use crate::source::{read_http_revision, REVISION}; @@ -21,7 +21,7 @@ pub struct RegistryWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, index_locations: &'a IndexLocations, - hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, index: FxHashMap<&'a PackageName, BTreeMap>, } @@ -31,13 +31,13 @@ impl<'a> RegistryWheelIndex<'a> { cache: &'a Cache, tags: &'a Tags, index_locations: &'a IndexLocations, - hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, ) -> Self { Self { cache, tags, index_locations, - hashes, + hasher, index: FxHashMap::default(), } } @@ -72,7 +72,7 @@ impl<'a> RegistryWheelIndex<'a> { self.cache, self.tags, self.index_locations, - self.hashes, + self.hasher, )), }; versions @@ -84,10 +84,9 @@ impl<'a> RegistryWheelIndex<'a> { cache: &Cache, tags: &Tags, index_locations: &IndexLocations, - hashes: &RequiredHashes, + hasher: &HashStrategy, ) -> BTreeMap { let mut versions = BTreeMap::new(); - let hashes = hashes.get(package).unwrap_or_default(); // Collect into owned `IndexUrl` let flat_index_urls: Vec = index_locations @@ -119,7 +118,7 @@ impl<'a> RegistryWheelIndex<'a> { { if let Some(wheel) = CachedWheel::from_http_pointer(&wheel_dir.join(&file)) { // Enforce hash-checking based on the built distribution. - if wheel.satisfies(hashes) { + if wheel.satisfies(hasher.get(package)) { Self::add_wheel(wheel, tags, &mut versions); } } @@ -132,7 +131,7 @@ impl<'a> RegistryWheelIndex<'a> { if let Some(wheel) = CachedWheel::from_revision_pointer(&wheel_dir.join(&file)) { // Enforce hash-checking based on the built distribution. - if wheel.satisfies(hashes) { + if wheel.satisfies(hasher.get(package)) { Self::add_wheel(wheel, tags, &mut versions); } } @@ -153,7 +152,7 @@ impl<'a> RegistryWheelIndex<'a> { let revision_entry = cache_shard.entry(REVISION); if let Ok(Some(revision)) = read_http_revision(&revision_entry) { // Enforce hash-checking based on the source distribution. - if revision.satisfies(hashes) { + if revision.satisfies(hasher.get(package)) { for wheel_dir in symlinks(cache_shard.join(revision.id())) { if let Some(wheel) = CachedWheel::from_built_source(&wheel_dir) { Self::add_wheel(wheel, tags, &mut versions); diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index b64f0025f..91172f821 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -16,8 +16,8 @@ use zip::ZipArchive; use distribution_filename::WheelFilename; use distribution_types::{ - BuildableSource, DirectArchiveUrl, Dist, FileLocation, GitSourceUrl, Hashed, LocalEditable, - PathSourceDist, PathSourceUrl, RemoteSource, SourceDist, SourceUrl, + BuildableSource, DirectArchiveUrl, Dist, FileLocation, GitSourceUrl, HashPolicy, Hashed, + LocalEditable, PathSourceDist, PathSourceUrl, RemoteSource, SourceDist, SourceUrl, }; use install_wheel_rs::metadata::read_archive_metadata; use platform_tags::Tags; @@ -79,7 +79,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, tags: &Tags, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { let built_wheel_metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { @@ -214,7 +214,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { pub(super) async fn download_and_build_metadata( &self, source: &BuildableSource<'_>, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { let metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { @@ -350,7 +350,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { cache_shard: &CacheShard, subdirectory: Option<&'data Path>, tags: &Tags, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Fetch the revision for the source distribution. let revision = self @@ -361,7 +361,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { if !revision.satisfies(hashes) { return Err(Error::hash_mismatch( source.to_string(), - hashes, + hashes.digests(), revision.hashes(), )); } @@ -418,7 +418,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { url: &'data Url, cache_shard: &CacheShard, subdirectory: Option<&'data Path>, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Fetch the revision for the source distribution. let revision = self @@ -429,7 +429,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { if !revision.satisfies(hashes) { return Err(Error::hash_mismatch( source.to_string(), - hashes, + hashes.digests(), revision.hashes(), )); } @@ -507,7 +507,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { filename: &str, url: &Url, cache_shard: &CacheShard, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { let cache_entry = cache_shard.entry(REVISION); let cache_control = match self.client.connectivity() { @@ -570,7 +570,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, tags: &Tags, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { let cache_shard = self.build_context.cache().shard( CacheBucket::BuiltWheels, @@ -586,7 +586,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { if !revision.satisfies(hashes) { return Err(Error::hash_mismatch( source.to_string(), - hashes, + hashes.digests(), revision.hashes(), )); } @@ -640,7 +640,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { let cache_shard = self.build_context.cache().shard( CacheBucket::BuiltWheels, @@ -656,7 +656,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { if !revision.satisfies(hashes) { return Err(Error::hash_mismatch( source.to_string(), - hashes, + hashes.digests(), revision.hashes(), )); } @@ -732,7 +732,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, cache_shard: &CacheShard, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Determine the last-modified time of the source distribution. let modified = ArchiveTimestamp::from_file(&resource.path).map_err(Error::CacheRead)?; @@ -779,10 +779,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, tags: &Tags, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Before running the build, check that the hashes match. - if !hashes.is_empty() { + if hashes.is_validate() { return Err(Error::HashesNotSupportedSourceTree(source.to_string())); } @@ -843,10 +843,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Before running the build, check that the hashes match. - if !hashes.is_empty() { + if hashes.is_validate() { return Err(Error::HashesNotSupportedSourceTree(source.to_string())); } @@ -945,10 +945,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &GitSourceUrl<'_>, tags: &Tags, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Before running the build, check that the hashes match. - if !hashes.is_empty() { + if hashes.is_validate() { return Err(Error::HashesNotSupportedGit(source.to_string())); } @@ -1017,10 +1017,10 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, resource: &GitSourceUrl<'_>, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result { // Before running the build, check that the hashes match. - if !hashes.is_empty() { + if hashes.is_validate() { return Err(Error::HashesNotSupportedGit(source.to_string())); } @@ -1111,7 +1111,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, filename: &str, target: &Path, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result, Error> { let temp_dir = tempfile::tempdir_in(self.build_context.cache().bucket(CacheBucket::BuiltWheels)) @@ -1122,12 +1122,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .into_async_read(); // Create a hasher for each hash algorithm. - let algorithms = { - let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); - hash.sort(); - hash.dedup(); - hash - }; + let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(reader.compat(), &mut hashers); @@ -1137,7 +1132,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { drop(span); // If necessary, exhaust the reader to compute the hash. - if !hashes.is_empty() { + if !hashes.is_none() { hasher.finish().await.map_err(Error::HashExhaustion)?; } @@ -1166,7 +1161,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, path: &Path, target: &Path, - hashes: &[HashDigest], + hashes: HashPolicy<'_>, ) -> Result, Error> { debug!("Unpacking for build: {}", path.display()); @@ -1178,12 +1173,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .map_err(Error::CacheRead)?; // Create a hasher for each hash algorithm. - let algorithms = { - let mut hash = hashes.iter().map(HashDigest::algorithm).collect::>(); - hash.sort(); - hash.dedup(); - hash - }; + let algorithms = hashes.algorithms(); let mut hashers = algorithms.into_iter().map(Hasher::from).collect::>(); let mut hasher = uv_extract::hash::HashReader::new(reader, &mut hashers); @@ -1191,7 +1181,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { uv_extract::stream::archive(&mut hasher, path, &temp_dir.path()).await?; // If necessary, exhaust the reader to compute the hash. - if !hashes.is_empty() { + if !hashes.is_none() { hasher.finish().await.map_err(Error::HashExhaustion)?; } diff --git a/crates/uv-installer/src/downloader.rs b/crates/uv-installer/src/downloader.rs index 1c60f46e5..5862bb210 100644 --- a/crates/uv-installer/src/downloader.rs +++ b/crates/uv-installer/src/downloader.rs @@ -15,7 +15,7 @@ use platform_tags::Tags; use uv_cache::Cache; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, LocalWheel}; -use uv_types::{BuildContext, InFlight, RequiredHashes}; +use uv_types::{BuildContext, HashStrategy, InFlight}; use crate::editable::BuiltEditable; @@ -40,7 +40,7 @@ pub enum Error { pub struct Downloader<'a, Context: BuildContext + Send + Sync> { tags: &'a Tags, cache: &'a Cache, - hashes: &'a RequiredHashes, + hashes: &'a HashStrategy, database: DistributionDatabase<'a, Context>, reporter: Option>, } @@ -49,7 +49,7 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { pub fn new( cache: &'a Cache, tags: &'a Tags, - hashes: &'a RequiredHashes, + hashes: &'a HashStrategy, client: &'a RegistryClient, build_context: &'a Context, ) -> Self { @@ -170,22 +170,22 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { pub async fn get_wheel(&self, dist: Dist, in_flight: &InFlight) -> Result { let id = dist.distribution_id(); if in_flight.downloads.register(id.clone()) { - let hashes = self.hashes.get(dist.name()).unwrap_or_default(); + let policy = self.hashes.get(dist.name()); let result = self .database - .get_or_build_wheel(&dist, self.tags, hashes) + .get_or_build_wheel(&dist, self.tags, policy) .boxed() .map_err(|err| Error::Fetch(dist.clone(), err)) .await .and_then(|wheel: LocalWheel| { - if wheel.satisfies(hashes) { + if wheel.satisfies(policy) { Ok(wheel) } else { Err(Error::Fetch( dist.clone(), uv_distribution::Error::hash_mismatch( dist.to_string(), - hashes, + policy.digests(), wheel.hashes(), ), )) diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 2151194ae..d9122b96f 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -19,7 +19,7 @@ use uv_configuration::{NoBinary, Reinstall}; use uv_distribution::{read_timestamped_archive, Archive, BuiltWheelIndex, RegistryWheelIndex}; use uv_fs::Simplified; use uv_interpreter::PythonEnvironment; -use uv_types::RequiredHashes; +use uv_types::HashStrategy; use crate::{ResolvedEditable, SitePackages}; @@ -66,15 +66,15 @@ impl<'a> Planner<'a> { mut site_packages: SitePackages<'_>, reinstall: &Reinstall, no_binary: &NoBinary, - hashes: &RequiredHashes, + hasher: &HashStrategy, index_locations: &IndexLocations, cache: &Cache, venv: &PythonEnvironment, tags: &Tags, ) -> Result { // Index all the already-downloaded wheels in the cache. - let mut registry_index = RegistryWheelIndex::new(cache, tags, index_locations, hashes); - let built_index = BuiltWheelIndex::new(cache, tags, hashes); + let mut registry_index = RegistryWheelIndex::new(cache, tags, index_locations, hasher); + let built_index = BuiltWheelIndex::new(cache, tags, hasher); let mut cached = vec![]; let mut remote = vec![]; @@ -262,8 +262,7 @@ impl<'a> Planner<'a> { let archive = rmp_serde::from_slice::(&data)?; // Enforce hash checking. - let hashes = hashes.get(&requirement.name).unwrap_or_default(); - if archive.satisfies(hashes) { + if archive.satisfies(hasher.get(&requirement.name)) { let cached_dist = CachedDirectUrlDist::from_url( wheel.filename, wheel.url, @@ -312,8 +311,7 @@ impl<'a> Planner<'a> { &cache_entry, ArchiveTimestamp::from_file(&wheel.path)?, )? { - let hashes = hashes.get(&requirement.name).unwrap_or_default(); - if archive.satisfies(hashes) { + if archive.satisfies(hasher.get(&requirement.name)) { let cached_dist = CachedDirectUrlDist::from_url( wheel.filename, wheel.url, diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index 31168bdf9..7d8ae63bf 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -12,7 +12,7 @@ use uv_client::RegistryClient; use uv_configuration::{Constraints, Overrides}; use uv_distribution::{DistributionDatabase, Reporter}; use uv_resolver::{InMemoryIndex, MetadataResponse}; -use uv_types::{BuildContext, RequestedRequirements, RequiredHashes}; +use uv_types::{BuildContext, HashStrategy, RequestedRequirements}; /// A resolver for resolving lookahead requirements from direct URLs. /// @@ -40,7 +40,7 @@ pub struct LookaheadResolver<'a, Context: BuildContext + Send + Sync> { /// The editable requirements for the project. editables: &'a [(LocalEditable, Metadata23)], /// The required hashes for the project. - hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, /// The in-memory index for resolving dependencies. index: &'a InMemoryIndex, /// The database for fetching and building distributions. @@ -55,7 +55,7 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { constraints: &'a Constraints, overrides: &'a Overrides, editables: &'a [(LocalEditable, Metadata23)], - hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, context: &'a Context, client: &'a RegistryClient, index: &'a InMemoryIndex, @@ -65,7 +65,7 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { constraints, overrides, editables, - hashes, + hasher, index, database: DistributionDatabase::new(client, context), } @@ -155,10 +155,9 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { archive.metadata.requires_dist.clone() } else { // Run the PEP 517 build process to extract metadata from the source distribution. - let hashes = self.hashes.get(dist.name()).unwrap_or_default(); let archive = self .database - .get_or_build_wheel_metadata(&dist, hashes) + .get_or_build_wheel_metadata(&dist, self.hasher.get(dist.name())) .await .with_context(|| match &dist { Dist::Built(built) => format!("Failed to download: {built}"), diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index b62dcc1e2..79ad535e0 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -5,13 +5,13 @@ use anyhow::{Context, Result}; use futures::{StreamExt, TryStreamExt}; use url::Url; -use distribution_types::{BuildableSource, PackageId, PathSourceUrl, SourceUrl}; +use distribution_types::{BuildableSource, HashPolicy, PackageId, PathSourceUrl, SourceUrl}; use pep508_rs::Requirement; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, Reporter}; use uv_fs::Simplified; use uv_resolver::{InMemoryIndex, MetadataResponse}; -use uv_types::BuildContext; +use uv_types::{BuildContext, HashStrategy}; use crate::ExtrasSpecification; @@ -24,8 +24,8 @@ pub struct SourceTreeResolver<'a, Context: BuildContext + Send + Sync> { source_trees: Vec, /// The extras to include when resolving requirements. extras: &'a ExtrasSpecification<'a>, - /// Whether to require hashes for all dependencies. - require_hashes: bool, + /// The hash policy to enforce. + hasher: &'a HashStrategy, /// The in-memory index for resolving dependencies. index: &'a InMemoryIndex, /// The database for fetching and building distributions. @@ -37,7 +37,7 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { pub fn new( source_trees: Vec, extras: &'a ExtrasSpecification<'a>, - require_hashes: bool, + hasher: &'a HashStrategy, context: &'a Context, client: &'a RegistryClient, index: &'a InMemoryIndex, @@ -45,7 +45,7 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { Self { source_trees, extras, - require_hashes, + hasher, index, database: DistributionDatabase::new(client, context), } @@ -87,15 +87,19 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { path: Cow::Owned(path), }); - // TODO(charlie): Should we enforce this earlier? If the metadata can be extracted - // statically, it won't go through this resolver. But we'll fail anyway, since the - // dependencies (when extracted from a `pyproject.toml` or `setup.py`) won't include hashes. - if self.require_hashes { - return Err(anyhow::anyhow!( - "Hash-checking is not supported for local directories: {}", - source_tree.user_display() - )); - } + // Determine the hash policy. Since we don't have a package name, we perform a + // manual match. + let hashes = match self.hasher { + HashStrategy::None => HashPolicy::None, + HashStrategy::Generate => HashPolicy::Generate, + HashStrategy::Validate(_) => { + // TODO(charlie): Support `--require-hashes` for unnamed requirements. + return Err(anyhow::anyhow!( + "Hash-checking is not supported for local directories: {}", + source_tree.user_display() + )); + } + }; // Fetch the metadata for the distribution. let metadata = { @@ -117,7 +121,7 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { } else { // Run the PEP 517 build process to extract metadata from the source distribution. let source = BuildableSource::Url(source); - let archive = self.database.build_wheel_metadata(&source, &[]).await?; + let archive = self.database.build_wheel_metadata(&source, hashes).await?; // Insert the metadata into the index. self.index diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index 5c4d4b3b5..deeb847e5 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -10,8 +10,8 @@ use tracing::debug; use distribution_filename::{SourceDistFilename, WheelFilename}; use distribution_types::{ - BuildableSource, DirectSourceUrl, GitSourceUrl, PackageId, PathSourceUrl, RemoteSource, - SourceUrl, + BuildableSource, DirectSourceUrl, GitSourceUrl, HashPolicy, PackageId, PathSourceUrl, + RemoteSource, SourceUrl, }; use pep508_rs::{ Requirement, RequirementsTxtRequirement, Scheme, UnnamedRequirement, VersionOrUrl, @@ -21,14 +21,14 @@ use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, Reporter}; use uv_normalize::PackageName; use uv_resolver::{InMemoryIndex, MetadataResponse}; -use uv_types::BuildContext; +use uv_types::{BuildContext, HashStrategy}; /// Like [`RequirementsSpecification`], but with concrete names for all requirements. pub struct NamedRequirementsResolver<'a, Context: BuildContext + Send + Sync> { /// The requirements for the project. requirements: Vec, /// Whether to check hashes for distributions. - require_hashes: bool, + hasher: &'a HashStrategy, /// The in-memory index for resolving dependencies. index: &'a InMemoryIndex, /// The database for fetching and building distributions. @@ -39,14 +39,14 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont /// Instantiate a new [`NamedRequirementsResolver`] for a given set of requirements. pub fn new( requirements: Vec, - require_hashes: bool, + hasher: &'a HashStrategy, context: &'a Context, client: &'a RegistryClient, index: &'a InMemoryIndex, ) -> Self { Self { requirements, - require_hashes, + hasher, index, database: DistributionDatabase::new(client, context), } @@ -65,7 +65,7 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont pub async fn resolve(self) -> Result> { let Self { requirements, - require_hashes, + hasher, index, database, } = self; @@ -74,8 +74,7 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont match requirement { RequirementsTxtRequirement::Pep508(requirement) => Ok(requirement), RequirementsTxtRequirement::Unnamed(requirement) => { - Self::resolve_requirement(requirement, require_hashes, index, &database) - .await + Self::resolve_requirement(requirement, hasher, index, &database).await } } }) @@ -87,7 +86,7 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont /// Infer the package name for a given "unnamed" requirement. async fn resolve_requirement( requirement: UnnamedRequirement, - require_hashes: bool, + hasher: &HashStrategy, index: &InMemoryIndex, database: &DistributionDatabase<'a, Context>, ) -> Result { @@ -240,13 +239,6 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont } }; - // TODO(charlie): Support `--require-hashes` for unnamed requirements. - if require_hashes { - return Err(anyhow::anyhow!( - "Unnamed requirements are not supported with `--require-hashes`" - )); - } - // Fetch the metadata for the distribution. let name = { let id = PackageId::from_url(source.url()); @@ -260,9 +252,22 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont // If the metadata is already in the index, return it. archive.metadata.name.clone() } else { + // Determine the hash policy. Since we don't have a package name, we perform a + // manual match. + let hashes = match hasher { + HashStrategy::None => HashPolicy::None, + HashStrategy::Generate => HashPolicy::Generate, + HashStrategy::Validate(_) => { + // TODO(charlie): Support `--require-hashes` for unnamed requirements. + return Err(anyhow::anyhow!( + "Unnamed requirements are not supported with `--require-hashes`" + )); + } + }; + // Run the PEP 517 build process to extract metadata from the source distribution. let source = BuildableSource::Url(source); - let archive = database.build_wheel_metadata(&source, &[]).await?; + let archive = database.build_wheel_metadata(&source, hashes).await?; let name = archive.metadata.name.clone(); diff --git a/crates/uv-resolver/src/flat_index.rs b/crates/uv-resolver/src/flat_index.rs index f11f9a2f7..4a0093471 100644 --- a/crates/uv-resolver/src/flat_index.rs +++ b/crates/uv-resolver/src/flat_index.rs @@ -6,8 +6,9 @@ use tracing::instrument; use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use distribution_types::{ - BuiltDist, Dist, File, Hash, IncompatibleSource, IncompatibleWheel, IndexUrl, PrioritizedDist, - RegistryBuiltDist, RegistrySourceDist, SourceDist, SourceDistCompatibility, WheelCompatibility, + BuiltDist, Dist, File, Hash, HashPolicy, IncompatibleSource, IncompatibleWheel, IndexUrl, + PrioritizedDist, RegistryBuiltDist, RegistrySourceDist, SourceDist, SourceDistCompatibility, + WheelCompatibility, }; use pep440_rs::Version; use platform_tags::{TagCompatibility, Tags}; @@ -15,7 +16,7 @@ use pypi_types::HashDigest; use uv_client::FlatIndexEntries; use uv_configuration::{NoBinary, NoBuild}; use uv_normalize::PackageName; -use uv_types::RequiredHashes; +use uv_types::HashStrategy; /// A set of [`PrioritizedDist`] from a `--find-links` entry, indexed by [`PackageName`] /// and [`Version`]. @@ -34,7 +35,7 @@ impl FlatIndex { pub fn from_entries( entries: FlatIndexEntries, tags: &Tags, - required_hashes: &RequiredHashes, + hasher: &HashStrategy, no_build: &NoBuild, no_binary: &NoBinary, ) -> Self { @@ -47,7 +48,7 @@ impl FlatIndex { file, filename, tags, - required_hashes, + hasher, no_build, no_binary, url, @@ -66,7 +67,7 @@ impl FlatIndex { file: File, filename: DistFilename, tags: &Tags, - required_hashes: &RequiredHashes, + hasher: &HashStrategy, no_build: &NoBuild, no_binary: &NoBinary, index: IndexUrl, @@ -77,13 +78,8 @@ impl FlatIndex { DistFilename::WheelFilename(filename) => { let version = filename.version.clone(); - let compatibility = Self::wheel_compatibility( - &filename, - &file.hashes, - tags, - required_hashes, - no_binary, - ); + let compatibility = + Self::wheel_compatibility(&filename, &file.hashes, tags, hasher, no_binary); let dist = Dist::Built(BuiltDist::Registry(RegistryBuiltDist { filename, file: Box::new(file), @@ -99,12 +95,8 @@ impl FlatIndex { } } DistFilename::SourceDistFilename(filename) => { - let compatibility = Self::source_dist_compatibility( - &filename, - &file.hashes, - required_hashes, - no_build, - ); + let compatibility = + Self::source_dist_compatibility(&filename, &file.hashes, hasher, no_build); let dist = Dist::Source(SourceDist::Registry(RegistrySourceDist { filename: filename.clone(), file: Box::new(file), @@ -125,7 +117,7 @@ impl FlatIndex { fn source_dist_compatibility( filename: &SourceDistFilename, hashes: &[HashDigest], - required_hashes: &RequiredHashes, + hasher: &HashStrategy, no_build: &NoBuild, ) -> SourceDistCompatibility { // Check if source distributions are allowed for this package. @@ -140,10 +132,10 @@ impl FlatIndex { } // Check if hashes line up - let hash = if let Some(required_hashes) = required_hashes.get(&filename.name) { + let hash = if let HashPolicy::Validate(required) = hasher.get(&filename.name) { if hashes.is_empty() { Hash::Missing - } else if hashes.iter().any(|hash| required_hashes.contains(hash)) { + } else if required.iter().any(|hash| hashes.contains(hash)) { Hash::Matched } else { Hash::Mismatched @@ -159,7 +151,7 @@ impl FlatIndex { filename: &WheelFilename, hashes: &[HashDigest], tags: &Tags, - required_hashes: &RequiredHashes, + hasher: &HashStrategy, no_binary: &NoBinary, ) -> WheelCompatibility { // Check if binaries are allowed for this package. @@ -182,10 +174,10 @@ impl FlatIndex { }; // Check if hashes line up - let hash = if let Some(required_hashes) = required_hashes.get(&filename.name) { + let hash = if let HashPolicy::Validate(required) = hasher.get(&filename.name) { if hashes.is_empty() { Hash::Missing - } else if hashes.iter().any(|hash| required_hashes.contains(hash)) { + } else if required.iter().any(|hash| hashes.contains(hash)) { Hash::Matched } else { Hash::Mismatched diff --git a/crates/uv-resolver/src/hash_checking_mode.rs b/crates/uv-resolver/src/hash_checking_mode.rs deleted file mode 100644 index 080939339..000000000 --- a/crates/uv-resolver/src/hash_checking_mode.rs +++ /dev/null @@ -1,15 +0,0 @@ -#[derive(Debug, Default, Clone, Copy)] -pub enum HashCheckingMode { - /// Hash-checking mode is disabled. - #[default] - Disabled, - /// Hash-checking mode is enabled. - Enabled, -} - -impl HashCheckingMode { - /// Returns `true` if hash-checking is enabled. - pub fn is_enabled(self) -> bool { - matches!(self, Self::Enabled) - } -} diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 4a1dad07f..c7fd1aea1 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -2,7 +2,6 @@ pub use dependency_mode::DependencyMode; pub use error::ResolveError; pub use exclusions::Exclusions; pub use flat_index::FlatIndex; -pub use hash_checking_mode::HashCheckingMode; pub use manifest::Manifest; pub use options::{Options, OptionsBuilder}; pub use preferences::{Preference, PreferenceError}; @@ -27,7 +26,6 @@ mod editables; mod error; mod exclusions; mod flat_index; -mod hash_checking_mode; mod manifest; mod options; mod pins; diff --git a/crates/uv-resolver/src/options.rs b/crates/uv-resolver/src/options.rs index bc9481fed..55e6e5f07 100644 --- a/crates/uv-resolver/src/options.rs +++ b/crates/uv-resolver/src/options.rs @@ -1,6 +1,5 @@ use chrono::{DateTime, Utc}; -use crate::hash_checking_mode::HashCheckingMode; use crate::{DependencyMode, PreReleaseMode, ResolutionMode}; /// Options for resolving a manifest. @@ -9,7 +8,6 @@ pub struct Options { pub resolution_mode: ResolutionMode, pub prerelease_mode: PreReleaseMode, pub dependency_mode: DependencyMode, - pub hash_checking_mode: HashCheckingMode, pub exclude_newer: Option>, } @@ -19,7 +17,6 @@ pub struct OptionsBuilder { resolution_mode: ResolutionMode, prerelease_mode: PreReleaseMode, dependency_mode: DependencyMode, - hash_checking_mode: HashCheckingMode, exclude_newer: Option>, } @@ -50,13 +47,6 @@ impl OptionsBuilder { self } - /// Sets the hash-checking mode. - #[must_use] - pub fn hash_checking_mode(mut self, hash_checking_mode: HashCheckingMode) -> Self { - self.hash_checking_mode = hash_checking_mode; - self - } - /// Sets the exclusion date. #[must_use] pub fn exclude_newer(mut self, exclude_newer: Option>) -> Self { @@ -70,7 +60,6 @@ impl OptionsBuilder { resolution_mode: self.resolution_mode, prerelease_mode: self.prerelease_mode, dependency_mode: self.dependency_mode, - hash_checking_mode: self.hash_checking_mode, exclude_newer: self.exclude_newer, } } diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index 2b69d5a4a..7203668b1 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -134,15 +134,13 @@ impl ResolutionGraph { .filter(|digests| !digests.is_empty()) { hashes.insert(package_name.clone(), digests.to_vec()); - } else if let Some(versions_response) = packages.get(package_name) { - if let VersionsResponse::Found(ref version_maps) = *versions_response { - for version_map in version_maps { - if let Some(mut digests) = version_map.hashes(version) { - digests.sort_unstable(); - hashes.insert(package_name.clone(), digests); - break; - } - } + } else if let Some(metadata_response) = + distributions.get(&pinned_package.package_id()) + { + if let MetadataResponse::Found(ref archive) = *metadata_response { + let mut digests = archive.hashes.clone(); + digests.sort_unstable(); + hashes.insert(package_name.clone(), digests); } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 4e1c89547..2ddf6c365 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -31,12 +31,11 @@ use uv_configuration::{Constraints, Overrides}; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; use uv_interpreter::Interpreter; use uv_normalize::PackageName; -use uv_types::{BuildContext, InstalledPackagesProvider, RequiredHashes}; +use uv_types::{BuildContext, HashStrategy, InstalledPackagesProvider}; use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::editables::Editables; use crate::error::ResolveError; -use crate::hash_checking_mode::HashCheckingMode; use crate::manifest::Manifest; use crate::pins::FilePins; use crate::preferences::Preferences; @@ -122,8 +121,7 @@ pub struct Resolver< urls: Urls, locals: Locals, dependency_mode: DependencyMode, - hash_checking_mode: HashCheckingMode, - hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, markers: &'a MarkerEnvironment, python_requirement: PythonRequirement, selector: CandidateSelector, @@ -158,7 +156,7 @@ impl< client: &'a RegistryClient, flat_index: &'a FlatIndex, index: &'a InMemoryIndex, - hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, build_context: &'a Context, installed_packages: &'a InstalledPackages, ) -> Result { @@ -169,7 +167,7 @@ impl< tags, PythonRequirement::new(interpreter, markers), AllowedYanks::from_manifest(&manifest, markers), - hashes, + hasher, options.exclude_newer, build_context.no_binary(), build_context.no_build(), @@ -177,7 +175,7 @@ impl< Self::new_custom_io( manifest, options, - hashes, + hasher, markers, PythonRequirement::new(interpreter, markers), index, @@ -198,7 +196,7 @@ impl< pub fn new_custom_io( manifest: Manifest, options: Options, - hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, markers: &'a MarkerEnvironment, python_requirement: PythonRequirement, index: &'a InMemoryIndex, @@ -212,7 +210,6 @@ impl< visited: DashSet::default(), selector: CandidateSelector::for_resolution(options, &manifest, markers), dependency_mode: options.dependency_mode, - hash_checking_mode: options.hash_checking_mode, urls: Urls::from_manifest(&manifest, markers)?, locals: Locals::from_manifest(&manifest, markers), project: manifest.project, @@ -222,7 +219,7 @@ impl< preferences: Preferences::from_iter(manifest.preferences, markers), exclusions: manifest.exclusions, editables: Editables::from_requirements(manifest.editables), - hashes, + hasher, markers, python_requirement, reporter: None, @@ -528,10 +525,8 @@ impl< PubGrubPackage::Python(_) => {} PubGrubPackage::Package(package_name, _extra, None) => { // Validate that the package is permitted under hash-checking mode. - if self.hash_checking_mode.is_enabled() { - if !self.hashes.contains(package_name) { - return Err(ResolveError::UnhashedPackage(package_name.clone())); - } + if !self.hasher.allows(package_name) { + return Err(ResolveError::UnhashedPackage(package_name.clone())); } // Emit a request to fetch the metadata for this package. @@ -544,10 +539,8 @@ impl< } PubGrubPackage::Package(package_name, _extra, Some(url)) => { // Validate that the package is permitted under hash-checking mode. - if self.hash_checking_mode.is_enabled() { - if !self.hashes.contains(package_name) { - return Err(ResolveError::UnhashedPackage(package_name.clone())); - } + if !self.hasher.allows(package_name) { + return Err(ResolveError::UnhashedPackage(package_name.clone())); } // Emit a request to fetch the metadata for this distribution. diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 7c26b9e69..566da4894 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -10,7 +10,7 @@ use uv_client::RegistryClient; use uv_configuration::{NoBinary, NoBuild}; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; use uv_normalize::PackageName; -use uv_types::{BuildContext, RequiredHashes}; +use uv_types::{BuildContext, HashStrategy}; use crate::flat_index::FlatIndex; use crate::python_requirement::PythonRequirement; @@ -83,7 +83,7 @@ pub struct DefaultResolverProvider<'a, Context: BuildContext + Send + Sync> { tags: Tags, python_requirement: PythonRequirement, allowed_yanks: AllowedYanks, - required_hashes: RequiredHashes, + hasher: HashStrategy, exclude_newer: Option>, no_binary: NoBinary, no_build: NoBuild, @@ -99,7 +99,7 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex tags: &'a Tags, python_requirement: PythonRequirement, allowed_yanks: AllowedYanks, - required_hashes: &'a RequiredHashes, + hasher: &'a HashStrategy, exclude_newer: Option>, no_binary: &'a NoBinary, no_build: &'a NoBuild, @@ -111,7 +111,7 @@ impl<'a, Context: BuildContext + Send + Sync> DefaultResolverProvider<'a, Contex tags: tags.clone(), python_requirement, allowed_yanks, - required_hashes: required_hashes.clone(), + hasher: hasher.clone(), exclude_newer, no_binary: no_binary.clone(), no_build: no_build.clone(), @@ -139,7 +139,7 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider &self.tags, &self.python_requirement, &self.allowed_yanks, - &self.required_hashes, + &self.hasher, self.exclude_newer.as_ref(), self.flat_index.get(package_name).cloned(), &self.no_binary, @@ -179,8 +179,11 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider /// Fetch the metadata for a distribution, building it if necessary. async fn get_or_build_wheel_metadata<'io>(&'io self, dist: &'io Dist) -> WheelMetadataResult { - let hashes = self.required_hashes.get(dist.name()).unwrap_or_default(); - match self.fetcher.get_or_build_wheel_metadata(dist, hashes).await { + match self + .fetcher + .get_or_build_wheel_metadata(dist, self.hasher.get(dist.name())) + .await + { Ok(metadata) => Ok(MetadataResponse::Found(metadata)), Err(err) => match err { uv_distribution::Error::Client(client) => match client.into_kind() { diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index 3b4cad944..e6309f00c 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -17,7 +17,7 @@ use pypi_types::{HashDigest, Yanked}; use uv_client::{OwnedArchive, SimpleMetadata, VersionFiles}; use uv_configuration::{NoBinary, NoBuild}; use uv_normalize::PackageName; -use uv_types::RequiredHashes; +use uv_types::HashStrategy; use uv_warnings::warn_user_once; use crate::flat_index::FlatDistributions; @@ -48,7 +48,7 @@ impl VersionMap { tags: &Tags, python_requirement: &PythonRequirement, allowed_yanks: &AllowedYanks, - required_hashes: &RequiredHashes, + hasher: &HashStrategy, exclude_newer: Option<&DateTime>, flat_index: Option, no_binary: &NoBinary, @@ -112,10 +112,7 @@ impl VersionMap { .allowed_versions(package_name) .cloned() .unwrap_or_default(); - let required_hashes = required_hashes - .get(package_name) - .unwrap_or_default() - .to_vec(); + let required_hashes = hasher.get(package_name).digests().to_vec(); Self { inner: VersionMapInner::Lazy(VersionMapLazy { map, diff --git a/crates/uv-resolver/tests/resolver.rs b/crates/uv-resolver/tests/resolver.rs index 93ca66436..9a4951e1b 100644 --- a/crates/uv-resolver/tests/resolver.rs +++ b/crates/uv-resolver/tests/resolver.rs @@ -22,7 +22,7 @@ use uv_resolver::{ OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; use uv_types::{ - BuildContext, BuildIsolation, EmptyInstalledPackages, RequiredHashes, SourceBuildTrait, + BuildContext, BuildIsolation, EmptyInstalledPackages, HashStrategy, SourceBuildTrait, }; // Exclude any packages uploaded after this date. @@ -125,7 +125,7 @@ async fn resolve( find_default_python(&Cache::temp().unwrap()).expect("Expected a python to be installed"); let interpreter = Interpreter::artificial(real_interpreter.platform().clone(), markers.clone()); let build_context = DummyContext::new(Cache::temp()?, interpreter.clone()); - let hashes = RequiredHashes::default(); + let hashes = HashStrategy::None; let installed_packages = EmptyInstalledPackages; let resolver = Resolver::new( manifest, diff --git a/crates/uv-types/src/hashes.rs b/crates/uv-types/src/hash.rs similarity index 57% rename from crates/uv-types/src/hashes.rs rename to crates/uv-types/src/hash.rs index 4f12c18c7..4e0e8532d 100644 --- a/crates/uv-types/src/hashes.rs +++ b/crates/uv-types/src/hash.rs @@ -1,3 +1,4 @@ +use distribution_types::HashPolicy; use rustc_hash::FxHashMap; use std::str::FromStr; @@ -5,18 +6,45 @@ use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use pypi_types::{HashDigest, HashError}; use uv_normalize::PackageName; -/// A set of package versions that are permitted, even if they're marked as yanked by the -/// relevant index. -#[derive(Debug, Default, Clone)] -pub struct RequiredHashes(FxHashMap>); +#[derive(Debug, Clone)] +pub enum HashStrategy { + /// No hash policy is specified. + None, + /// Hashes should be generated (specifically, a SHA-256 hash), but not validated. + Generate, + /// Hashes should be validated against a pre-defined list of hashes. If necessary, hashes should + /// be generated so as to ensure that the archive is valid. + Validate(FxHashMap>), +} -impl RequiredHashes { - /// Generate the [`RequiredHashes`] from a set of requirement entries. +impl HashStrategy { + /// Return the [`HashPolicy`] for the given package. + pub fn get(&self, package_name: &PackageName) -> HashPolicy { + match self { + Self::None => HashPolicy::None, + Self::Generate => HashPolicy::Generate, + Self::Validate(hashes) => hashes + .get(package_name) + .map(Vec::as_slice) + .map_or(HashPolicy::None, HashPolicy::Validate), + } + } + + /// Returns `true` if the given package is allowed. + pub fn allows(&self, package_name: &PackageName) -> bool { + match self { + Self::None => true, + Self::Generate => true, + Self::Validate(hashes) => hashes.contains_key(package_name), + } + } + + /// Generate the required hashes from a set of [`Requirement`] entries. pub fn from_requirements( requirements: impl Iterator)>, markers: &MarkerEnvironment, - ) -> Result { - let mut allowed_hashes = FxHashMap::>::default(); + ) -> Result { + let mut hashes = FxHashMap::>::default(); // For each requirement, map from name to allowed hashes. We use the last entry for each // package. @@ -26,7 +54,7 @@ impl RequiredHashes { // // TODO(charlie): Preserve hashes from `requirements.txt` through to this pass, so that we // can iterate over requirements directly, rather than iterating over the entries. - for (requirement, hashes) in requirements { + for (requirement, digests) in requirements { if !requirement.evaluate_markers(markers, &[]) { continue; } @@ -43,51 +71,40 @@ impl RequiredHashes { { // Pinned versions are allowed. } else { - return Err(RequiredHashesError::UnpinnedRequirement( + return Err(HashStrategyError::UnpinnedRequirement( requirement.to_string(), )); } } None => { - return Err(RequiredHashesError::UnpinnedRequirement( + return Err(HashStrategyError::UnpinnedRequirement( requirement.to_string(), )) } } // Every requirement must include a hash. - if hashes.is_empty() { - return Err(RequiredHashesError::MissingHashes(requirement.to_string())); + if digests.is_empty() { + return Err(HashStrategyError::MissingHashes(requirement.to_string())); } // Parse the hashes. - let hashes = hashes + let digests = digests .iter() - .map(|hash| HashDigest::from_str(hash)) + .map(|digest| HashDigest::from_str(digest)) .collect::, _>>() .unwrap(); // TODO(charlie): Extract hashes from URL fragments. - allowed_hashes.insert(requirement.name, hashes); + hashes.insert(requirement.name, digests); } - Ok(Self(allowed_hashes)) - } - - /// Returns versions for the given package which are allowed even if marked as yanked by the - /// relevant index. - pub fn get(&self, package_name: &PackageName) -> Option<&[HashDigest]> { - self.0.get(package_name).map(Vec::as_slice) - } - - /// Returns whether the given package is allowed even if marked as yanked by the relevant index. - pub fn contains(&self, package_name: &PackageName) -> bool { - self.0.contains_key(package_name) + Ok(Self::Validate(hashes)) } } #[derive(thiserror::Error, Debug)] -pub enum RequiredHashesError { +pub enum HashStrategyError { #[error(transparent)] Hash(#[from] HashError), #[error("Unnamed requirements are not supported in `--require-hashes`")] diff --git a/crates/uv-types/src/lib.rs b/crates/uv-types/src/lib.rs index 281e685a5..df0b6c4a8 100644 --- a/crates/uv-types/src/lib.rs +++ b/crates/uv-types/src/lib.rs @@ -1,12 +1,12 @@ //! Fundamental types shared across `uv` crates. pub use builds::*; pub use downloads::*; -pub use hashes::*; +pub use hash::*; pub use requirements::*; pub use traits::*; mod builds; mod downloads; -mod hashes; +mod hash; mod requirements; mod traits; diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index c1388e6b2..8763806af 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -38,7 +38,7 @@ use uv_resolver::{ Manifest, OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver, }; use uv_toolchain::PythonVersion; -use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight, RequiredHashes}; +use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, ResolverReporter}; @@ -199,8 +199,12 @@ pub(crate) async fn pip_compile( |python_version| Cow::Owned(python_version.markers(interpreter.markers())), ); - // Don't enforce hashes during resolution. - let hashes = RequiredHashes::default(); + // Generate, but don't enforce hashes for the requirements. + let hasher = if generate_hashes { + HashStrategy::Generate + } else { + HashStrategy::None + }; // Incorporate any index locations from the provided sources. let index_locations = @@ -233,7 +237,7 @@ pub(crate) async fn pip_compile( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, &tags, &hashes, &no_build, &NoBinary::None) + FlatIndex::from_entries(entries, &tags, &hasher, &no_build, &NoBinary::None) }; // Track in-flight downloads, builds, etc., across resolutions. @@ -272,7 +276,7 @@ pub(crate) async fn pip_compile( // Convert from unnamed to named requirements. let mut requirements = NamedRequirementsResolver::new( requirements, - false, + &hasher, &build_dispatch, &client, &top_level_index, @@ -287,7 +291,7 @@ pub(crate) async fn pip_compile( SourceTreeResolver::new( source_trees, &extras, - false, + &hasher, &build_dispatch, &client, &top_level_index, @@ -312,7 +316,7 @@ pub(crate) async fn pip_compile( LocalEditable { url, path, extras } })); - let downloader = Downloader::new(&cache, &tags, &hashes, &client, &build_dispatch) + let downloader = Downloader::new(&cache, &tags, &hasher, &client, &build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64)); // Build all editables. @@ -360,7 +364,7 @@ pub(crate) async fn pip_compile( &constraints, &overrides, &editables, - &hashes, + &hasher, &build_dispatch, &client, &top_level_index, @@ -399,7 +403,7 @@ pub(crate) async fn pip_compile( &client, &flat_index, &top_level_index, - &hashes, + &hasher, &build_dispatch, &EmptyInstalledPackages, )? diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index 040f2ba87..cffa3d8ea 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -1,4 +1,5 @@ use std::fmt::Write; + use std::path::Path; use anstream::eprint; @@ -37,10 +38,10 @@ use uv_requirements::{ RequirementsSpecification, SourceTreeResolver, }; use uv_resolver::{ - DependencyMode, Exclusions, FlatIndex, HashCheckingMode, InMemoryIndex, Manifest, Options, - OptionsBuilder, PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, + DependencyMode, Exclusions, FlatIndex, InMemoryIndex, Manifest, Options, OptionsBuilder, + PreReleaseMode, Preference, ResolutionGraph, ResolutionMode, Resolver, }; -use uv_types::{BuildIsolation, InFlight, RequiredHashes}; +use uv_types::{BuildIsolation, HashStrategy, InFlight}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; @@ -186,8 +187,8 @@ pub(crate) async fn pip_install( let markers = venv.interpreter().markers(); // Collect the set of required hashes. - let hashes = if require_hashes { - RequiredHashes::from_requirements( + let hasher = if require_hashes { + HashStrategy::from_requirements( entries .into_iter() .filter_map(|requirement| match requirement.requirement { @@ -197,7 +198,7 @@ pub(crate) async fn pip_install( markers, )? } else { - RequiredHashes::default() + HashStrategy::None }; // Incorporate any index locations from the provided sources. @@ -224,7 +225,7 @@ pub(crate) async fn pip_install( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, tags, &hashes, &no_build, &no_binary) + FlatIndex::from_entries(entries, tags, &hasher, &no_build, &no_binary) }; // Determine whether to enable build isolation. @@ -266,7 +267,7 @@ pub(crate) async fn pip_install( // Convert from unnamed to named requirements. let mut requirements = NamedRequirementsResolver::new( requirements, - require_hashes, + &hasher, &resolve_dispatch, &client, &index, @@ -281,7 +282,7 @@ pub(crate) async fn pip_install( SourceTreeResolver::new( source_trees, extras, - require_hashes, + &hasher, &resolve_dispatch, &client, &index, @@ -306,7 +307,7 @@ pub(crate) async fn pip_install( build_editables( &editables, editable_wheel_dir.path(), - &hashes, + &hasher, &cache, &interpreter, tags, @@ -321,11 +322,6 @@ pub(crate) async fn pip_install( .resolution_mode(resolution_mode) .prerelease_mode(prerelease_mode) .dependency_mode(dependency_mode) - .hash_checking_mode(if require_hashes { - HashCheckingMode::Enabled - } else { - HashCheckingMode::Disabled - }) .exclude_newer(exclude_newer) .build(); @@ -336,7 +332,7 @@ pub(crate) async fn pip_install( overrides, project, &editables, - &hashes, + &hasher, &site_packages, &reinstall, &upgrade, @@ -397,7 +393,7 @@ pub(crate) async fn pip_install( link_mode, compile, &index_locations, - &hashes, + &hasher, tags, &client, &in_flight, @@ -473,7 +469,7 @@ async fn read_requirements( async fn build_editables( editables: &[EditableRequirement], editable_wheel_dir: &Path, - hashes: &RequiredHashes, + hasher: &HashStrategy, cache: &Cache, interpreter: &Interpreter, tags: &Tags, @@ -483,7 +479,7 @@ async fn build_editables( ) -> Result, Error> { let start = std::time::Instant::now(); - let downloader = Downloader::new(cache, tags, hashes, client, build_dispatch) + let downloader = Downloader::new(cache, tags, hasher, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(editables.len() as u64)); let editables = LocalEditables::from_editables(editables.iter().map(|editable| { @@ -540,7 +536,7 @@ async fn resolve( overrides: Vec, project: Option, editables: &[BuiltEditable], - hashes: &RequiredHashes, + hasher: &HashStrategy, site_packages: &SitePackages<'_>, reinstall: &Reinstall, upgrade: &Upgrade, @@ -587,7 +583,7 @@ async fn resolve( &constraints, &overrides, &editables, - hashes, + hasher, build_dispatch, client, index, @@ -618,7 +614,7 @@ async fn resolve( client, flat_index, index, - hashes, + hasher, build_dispatch, site_packages, )? @@ -662,7 +658,7 @@ async fn install( link_mode: LinkMode, compile: bool, index_urls: &IndexLocations, - hashes: &RequiredHashes, + hasher: &HashStrategy, tags: &Tags, client: &RegistryClient, in_flight: &InFlight, @@ -690,7 +686,7 @@ async fn install( site_packages, reinstall, no_binary, - hashes, + hasher, index_urls, cache, venv, @@ -743,7 +739,7 @@ async fn install( } else { let start = std::time::Instant::now(); - let downloader = Downloader::new(cache, tags, hashes, client, build_dispatch) + let downloader = Downloader::new(cache, tags, hasher, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64)); let wheels = downloader @@ -1060,7 +1056,7 @@ enum Error { Platform(#[from] platform_tags::PlatformError), #[error(transparent)] - RequiredHashes(#[from] uv_types::RequiredHashesError), + Hash(#[from] uv_types::HashStrategyError), #[error(transparent)] Io(#[from] std::io::Error), diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 093954682..2d401a646 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -30,10 +30,8 @@ use uv_requirements::{ ExtrasSpecification, NamedRequirementsResolver, RequirementsSource, RequirementsSpecification, SourceTreeResolver, }; -use uv_resolver::{ - DependencyMode, FlatIndex, HashCheckingMode, InMemoryIndex, Manifest, OptionsBuilder, Resolver, -}; -use uv_types::{BuildIsolation, EmptyInstalledPackages, InFlight, RequiredHashes}; +use uv_resolver::{DependencyMode, FlatIndex, InMemoryIndex, Manifest, OptionsBuilder, Resolver}; +use uv_types::{BuildIsolation, EmptyInstalledPackages, HashStrategy, InFlight}; use uv_warnings::warn_user; use crate::commands::reporters::{DownloadReporter, InstallReporter, ResolverReporter}; @@ -138,8 +136,8 @@ pub(crate) async fn pip_sync( let markers = venv.interpreter().markers(); // Collect the set of required hashes. - let hashes = if require_hashes { - RequiredHashes::from_requirements( + let hasher = if require_hashes { + HashStrategy::from_requirements( entries .into_iter() .filter_map(|requirement| match requirement.requirement { @@ -149,7 +147,7 @@ pub(crate) async fn pip_sync( markers, )? } else { - RequiredHashes::default() + HashStrategy::None }; // Incorporate any index locations from the provided sources. @@ -176,7 +174,7 @@ pub(crate) async fn pip_sync( let flat_index = { let client = FlatIndexClient::new(&client, &cache); let entries = client.fetch(index_locations.flat_index()).await?; - FlatIndex::from_entries(entries, tags, &hashes, &no_build, &no_binary) + FlatIndex::from_entries(entries, tags, &hasher, &no_build, &no_binary) }; // Create a shared in-memory index. @@ -218,16 +216,11 @@ pub(crate) async fn pip_sync( // Convert from unnamed to named requirements. let requirements = { // Convert from unnamed to named requirements. - let mut requirements = NamedRequirementsResolver::new( - requirements, - require_hashes, - &build_dispatch, - &client, - &index, - ) - .with_reporter(ResolverReporter::from(printer)) - .resolve() - .await?; + let mut requirements = + NamedRequirementsResolver::new(requirements, &hasher, &build_dispatch, &client, &index) + .with_reporter(ResolverReporter::from(printer)) + .resolve() + .await?; // Resolve any source trees into requirements. if !source_trees.is_empty() { @@ -235,7 +228,7 @@ pub(crate) async fn pip_sync( SourceTreeResolver::new( source_trees, &ExtrasSpecification::None, - require_hashes, + &hasher, &build_dispatch, &client, &index, @@ -254,7 +247,7 @@ pub(crate) async fn pip_sync( editables, &site_packages, reinstall, - &hashes, + &hasher, venv.interpreter(), tags, &cache, @@ -278,7 +271,7 @@ pub(crate) async fn pip_sync( site_packages, reinstall, &no_binary, - &hashes, + &hasher, &index_locations, &cache, &venv, @@ -317,11 +310,6 @@ pub(crate) async fn pip_sync( // Resolve with `--no-deps`. let options = OptionsBuilder::new() .dependency_mode(DependencyMode::Direct) - .hash_checking_mode(if require_hashes { - HashCheckingMode::Enabled - } else { - HashCheckingMode::Disabled - }) .build(); // Create a bound on the progress bar, since we know the number of packages upfront. @@ -337,7 +325,7 @@ pub(crate) async fn pip_sync( &client, &flat_index, &index, - &hashes, + &hasher, &build_dispatch, // TODO(zanieb): We should consider support for installed packages in pip sync &EmptyInstalledPackages, @@ -381,7 +369,7 @@ pub(crate) async fn pip_sync( } else { let start = std::time::Instant::now(); - let downloader = Downloader::new(&cache, tags, &hashes, &client, &build_dispatch) + let downloader = Downloader::new(&cache, tags, &hasher, &client, &build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(remote.len() as u64)); let wheels = downloader @@ -577,7 +565,7 @@ async fn resolve_editables( editables: Vec, site_packages: &SitePackages<'_>, reinstall: &Reinstall, - hashes: &RequiredHashes, + hasher: &HashStrategy, interpreter: &Interpreter, tags: &Tags, cache: &Cache, @@ -644,7 +632,7 @@ async fn resolve_editables( } else { let start = std::time::Instant::now(); - let downloader = Downloader::new(cache, tags, hashes, client, build_dispatch) + let downloader = Downloader::new(cache, tags, hasher, client, build_dispatch) .with_reporter(DownloadReporter::from(printer).with_length(uninstalled.len() as u64)); let editables = LocalEditables::from_editables(uninstalled.iter().map(|editable| { diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index d90bcb414..6f44fecbe 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -21,7 +21,7 @@ use uv_dispatch::BuildDispatch; use uv_fs::Simplified; use uv_interpreter::{find_default_python, find_requested_python, Error}; use uv_resolver::{FlatIndex, InMemoryIndex, OptionsBuilder}; -use uv_types::{BuildContext, BuildIsolation, InFlight, RequiredHashes}; +use uv_types::{BuildContext, BuildIsolation, HashStrategy, InFlight}; use crate::commands::ExitStatus; use crate::printer::Printer; @@ -170,7 +170,7 @@ async fn venv_impl( FlatIndex::from_entries( entries, tags, - &RequiredHashes::default(), + &HashStrategy::None, &NoBuild::All, &NoBinary::None, ) diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 2c153868e..87fcda443 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -3447,33 +3447,14 @@ fn compile_legacy_sdist_setuptools() -> Result<()> { Ok(()) } -/// Include hashes in the generated output. +/// Include hashes from the registry in the generated output. #[test] -fn generate_hashes() -> Result<()> { +fn generate_hashes_registry() -> Result<()> { let context = TestContext::new("3.12"); let requirements_in = context.temp_dir.child("requirements.in"); - requirements_in.write_str("flask==3.0.0")?; + requirements_in.write_str("anyio==4.0.0")?; - let colorama_locked = regex::escape(indoc! {r" - colorama==0.4.6 \ - --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ - --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 - # via click - "}); - let filters: Vec<_> = if cfg!(windows) { - // Remove colorama - vec![ - (colorama_locked.as_str(), ""), - ("Resolved 8 packages", "Resolved 7 packages"), - ] - } else { - vec![] - } - .into_iter() - .chain(context.filters()) - .collect(); - - uv_snapshot!(filters, context.compile() + uv_snapshot!(context.compile() .arg("requirements.in") .arg("--generate-hashes"), @r###" success: true @@ -3481,102 +3462,170 @@ fn generate_hashes() -> Result<()> { ----- stdout ----- # This file was autogenerated by uv via the following command: # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --generate-hashes - blinker==1.7.0 \ - --hash=sha256:c3f865d4d54db7abc53758a01601cf343fe55b84c1de4e3fa910e420b438d5b9 \ - --hash=sha256:e6820ff6fa4e4d1d8e2747c2283749c3f547e4fee112b98555cdcdae32996182 - # via flask - click==8.1.7 \ - --hash=sha256:ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28 \ - --hash=sha256:ca9853ad459e787e2192211578cc907e7594e294c7ccc834310722b41b9ca6de - # via flask - flask==3.0.0 \ - --hash=sha256:21128f47e4e3b9d597a3e8521a329bf56909b690fcc3fa3e477725aa81367638 \ - --hash=sha256:cfadcdb638b609361d29ec22360d6070a77d7463dcb3ab08d2c2f2f168845f58 - itsdangerous==2.1.2 \ - --hash=sha256:2c2349112351b88699d8d4b6b075022c0808887cb7ad10069318a8b0bc88db44 \ - --hash=sha256:5dbbc68b317e5e42f327f9021763545dc3fc3bfe22e6deb96aaf1fc38874156a - # via flask - jinja2==3.1.3 \ - --hash=sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa \ - --hash=sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90 - # via flask - markupsafe==2.1.5 \ - --hash=sha256:00e046b6dd71aa03a41079792f8473dc494d564611a8f89bbbd7cb93295ebdcf \ - --hash=sha256:075202fa5b72c86ad32dc7d0b56024ebdbcf2048c0ba09f1cde31bfdd57bcfff \ - --hash=sha256:0e397ac966fdf721b2c528cf028494e86172b4feba51d65f81ffd65c63798f3f \ - --hash=sha256:17b950fccb810b3293638215058e432159d2b71005c74371d784862b7e4683f3 \ - --hash=sha256:1f3fbcb7ef1f16e48246f704ab79d79da8a46891e2da03f8783a5b6fa41a9532 \ - --hash=sha256:2174c595a0d73a3080ca3257b40096db99799265e1c27cc5a610743acd86d62f \ - --hash=sha256:2b7c57a4dfc4f16f7142221afe5ba4e093e09e728ca65c51f5620c9aaeb9a617 \ - --hash=sha256:2d2d793e36e230fd32babe143b04cec8a8b3eb8a3122d2aceb4a371e6b09b8df \ - --hash=sha256:30b600cf0a7ac9234b2638fbc0fb6158ba5bdcdf46aeb631ead21248b9affbc4 \ - --hash=sha256:397081c1a0bfb5124355710fe79478cdbeb39626492b15d399526ae53422b906 \ - --hash=sha256:3a57fdd7ce31c7ff06cdfbf31dafa96cc533c21e443d57f5b1ecc6cdc668ec7f \ - --hash=sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4 \ - --hash=sha256:3e53af139f8579a6d5f7b76549125f0d94d7e630761a2111bc431fd820e163b8 \ - --hash=sha256:4096e9de5c6fdf43fb4f04c26fb114f61ef0bf2e5604b6ee3019d51b69e8c371 \ - --hash=sha256:4275d846e41ecefa46e2015117a9f491e57a71ddd59bbead77e904dc02b1bed2 \ - --hash=sha256:4c31f53cdae6ecfa91a77820e8b151dba54ab528ba65dfd235c80b086d68a465 \ - --hash=sha256:4f11aa001c540f62c6166c7726f71f7573b52c68c31f014c25cc7901deea0b52 \ - --hash=sha256:5049256f536511ee3f7e1b3f87d1d1209d327e818e6ae1365e8653d7e3abb6a6 \ - --hash=sha256:58c98fee265677f63a4385256a6d7683ab1832f3ddd1e66fe948d5880c21a169 \ - --hash=sha256:598e3276b64aff0e7b3451b72e94fa3c238d452e7ddcd893c3ab324717456bad \ - --hash=sha256:5b7b716f97b52c5a14bffdf688f971b2d5ef4029127f1ad7a513973cfd818df2 \ - --hash=sha256:5dedb4db619ba5a2787a94d877bc8ffc0566f92a01c0ef214865e54ecc9ee5e0 \ - --hash=sha256:619bc166c4f2de5caa5a633b8b7326fbe98e0ccbfacabd87268a2b15ff73a029 \ - --hash=sha256:629ddd2ca402ae6dbedfceeba9c46d5f7b2a61d9749597d4307f943ef198fc1f \ - --hash=sha256:656f7526c69fac7f600bd1f400991cc282b417d17539a1b228617081106feb4a \ - --hash=sha256:6ec585f69cec0aa07d945b20805be741395e28ac1627333b1c5b0105962ffced \ - --hash=sha256:72b6be590cc35924b02c78ef34b467da4ba07e4e0f0454a2c5907f473fc50ce5 \ - --hash=sha256:7502934a33b54030eaf1194c21c692a534196063db72176b0c4028e140f8f32c \ - --hash=sha256:7a68b554d356a91cce1236aa7682dc01df0edba8d043fd1ce607c49dd3c1edcf \ - --hash=sha256:7b2e5a267c855eea6b4283940daa6e88a285f5f2a67f2220203786dfa59b37e9 \ - --hash=sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb \ - --hash=sha256:8590b4ae07a35970728874632fed7bd57b26b0102df2d2b233b6d9d82f6c62ad \ - --hash=sha256:8dd717634f5a044f860435c1d8c16a270ddf0ef8588d4887037c5028b859b0c3 \ - --hash=sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1 \ - --hash=sha256:97cafb1f3cbcd3fd2b6fbfb99ae11cdb14deea0736fc2b0952ee177f2b813a46 \ - --hash=sha256:a17a92de5231666cfbe003f0e4b9b3a7ae3afb1ec2845aadc2bacc93ff85febc \ - --hash=sha256:a549b9c31bec33820e885335b451286e2969a2d9e24879f83fe904a5ce59d70a \ - --hash=sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee \ - --hash=sha256:ae2ad8ae6ebee9d2d94b17fb62763125f3f374c25618198f40cbb8b525411900 \ - --hash=sha256:b91c037585eba9095565a3556f611e3cbfaa42ca1e865f7b8015fe5c7336d5a5 \ - --hash=sha256:bc1667f8b83f48511b94671e0e441401371dfd0f0a795c7daa4a3cd1dde55bea \ - --hash=sha256:bec0a414d016ac1a18862a519e54b2fd0fc8bbfd6890376898a6c0891dd82e9f \ - --hash=sha256:bf50cd79a75d181c9181df03572cdce0fbb75cc353bc350712073108cba98de5 \ - --hash=sha256:bff1b4290a66b490a2f4719358c0cdcd9bafb6b8f061e45c7a2460866bf50c2e \ - --hash=sha256:c061bb86a71b42465156a3ee7bd58c8c2ceacdbeb95d05a99893e08b8467359a \ - --hash=sha256:c8b29db45f8fe46ad280a7294f5c3ec36dbac9491f2d1c17345be8e69cc5928f \ - --hash=sha256:ce409136744f6521e39fd8e2a24c53fa18ad67aa5bc7c2cf83645cce5b5c4e50 \ - --hash=sha256:d050b3361367a06d752db6ead6e7edeb0009be66bc3bae0ee9d97fb326badc2a \ - --hash=sha256:d283d37a890ba4c1ae73ffadf8046435c76e7bc2247bbb63c00bd1a709c6544b \ - --hash=sha256:d9fad5155d72433c921b782e58892377c44bd6252b5af2f67f16b194987338a4 \ - --hash=sha256:daa4ee5a243f0f20d528d939d06670a298dd39b1ad5f8a72a4275124a7819eff \ - --hash=sha256:db0b55e0f3cc0be60c1f19efdde9a637c32740486004f20d1cff53c3c0ece4d2 \ - --hash=sha256:e61659ba32cf2cf1481e575d0462554625196a1f2fc06a1c777d3f48e8865d46 \ - --hash=sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b \ - --hash=sha256:ec6a563cff360b50eed26f13adc43e61bc0c04d94b8be985e6fb24b81f6dcfdf \ - --hash=sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5 \ - --hash=sha256:fa173ec60341d6bb97a89f5ea19c85c5643c1e7dedebc22f5181eb73573142c5 \ - --hash=sha256:fa9db3f79de01457b03d4f01b34cf91bc0048eb2c3846ff26f66687c2f6d16ab \ - --hash=sha256:fce659a462a1be54d2ffcacea5e3ba2d74daa74f30f5f143fe0c58636e355fdd \ - --hash=sha256:ffee1f21e5ef0d712f9033568f8344d5da8cc2869dbd08d87c84656e6a2d2f68 - # via - # jinja2 - # werkzeug - werkzeug==3.0.1 \ - --hash=sha256:507e811ecea72b18a404947aded4b3390e1db8f826b494d76550ef45bb3b1dcc \ - --hash=sha256:90a285dc0e42ad56b34e696398b8122ee4c681833fb35b8334a095d82c56da10 - # via flask + anyio==4.0.0 \ + --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f \ + --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio ----- stderr ----- - Resolved 7 packages in [TIME] + Resolved 3 packages in [TIME] "### ); Ok(()) } +/// Include hashes from the URL in the generated output. +#[test] +fn generate_hashes_source_distribution_url() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--generate-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --generate-hashes + anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz \ + --hash=sha256:e1875bb4b4e2de1669f4bc7869b6d3f54231cdced71605e6e64c9be77e3be50f + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Include hashes from the URL in the generated output. +#[test] +fn generate_hashes_built_distribution_url() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--generate-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --generate-hashes + anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl \ + --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Given an unnamed URL, include hashes for the URL and its dependencies. +#[test] +fn generate_hashes_unnamed_url() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--generate-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --generate-hashes + anyio @ https://files.pythonhosted.org/packages/14/fd/2f20c40b45e4fb4324834aea24bd4afdf1143390242c0b33774da0e2e34f/anyio-4.3.0-py3-none-any.whl \ + --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Given a local directory, include hashes for its dependencies, but not the directory itself. +#[test] +fn generate_hashes_local_directory() -> Result<()> { + let _context = TestContext::new("3.12"); + + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str(indoc! {r" + ../../scripts/packages/poetry_editable + " + })?; + + uv_snapshot!(context.filters(), context.compile() + .arg(requirements_in.path()) + .arg("--generate-hashes") + .current_dir(current_dir()?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in --generate-hashes + anyio==4.3.0 \ + --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \ + --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 + # via poetry-editable + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + poetry-editable @ ../../scripts/packages/poetry_editable + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + Ok(()) +} + /// Compile using `--find-links` with a local directory. #[test] fn find_links_directory() -> Result<()> { From 5583b90c30d3b85079c17fa51a5bfafc2ed5042f Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 17:30:27 -0400 Subject: [PATCH 095/110] Create dedicated abstractions for `.rev` and `.http` pointers (#2977) ## Summary This PR formalizes some of the concepts we use in the cache for "pointers to things". In the wheel cache, we have files like `annotated_types-0.6.0-py3-none-any.http`. This represents an unzipped wheel, cached alongside an HTTP caching policy. We now have a struct for this to encapsulate the logic: `HttpArchivePointer`. Similarly, we have files like `annotated_types-0.6.0-py3-none-any.rev`. This represents an unzipped local wheel, alongside with a timestamp. We now have a struct for this to encapsulate the logic: `LocalArchivePointer`. We have similar structs for source distributions too. --- crates/uv-cache/src/lib.rs | 6 + .../src/distribution_database.rs | 123 +++++++++---- .../src/index/built_wheel_index.rs | 21 ++- .../uv-distribution/src/index/cached_wheel.rs | 18 +- .../src/index/registry_wheel_index.rs | 10 +- crates/uv-distribution/src/lib.rs | 2 +- crates/uv-distribution/src/source/mod.rs | 165 ++++++++++-------- crates/uv-installer/src/plan.rs | 73 ++++---- 8 files changed, 249 insertions(+), 169 deletions(-) diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index ec054e83c..e232d5992 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -71,6 +71,12 @@ impl CacheEntry { } } +impl AsRef for CacheEntry { + fn as_ref(&self) -> &Path { + &self.0 + } +} + /// A subdirectory within the cache. #[derive(Debug, Clone)] pub struct CacheShard(PathBuf); diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 1f98abee8..04c941a5d 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -16,8 +16,10 @@ use distribution_types::{ }; use platform_tags::Tags; use pypi_types::{HashDigest, Metadata23}; -use uv_cache::{ArchiveTimestamp, CacheBucket, CacheEntry, CachedByTimestamp, WheelCache}; -use uv_client::{CacheControl, CachedClientError, Connectivity, RegistryClient}; +use uv_cache::{ArchiveTimestamp, CacheBucket, CacheEntry, Timestamp, WheelCache}; +use uv_client::{ + CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, +}; use uv_configuration::{NoBinary, NoBuild}; use uv_extract::hash::Hasher; use uv_fs::write_atomic; @@ -178,7 +180,6 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> WheelCache::Index(&wheel.index).wheel_dir(wheel.name().as_ref()), wheel.filename.stem(), ); - return self .load_wheel(path, &wheel.filename, cache_entry, dist, hashes) .await; @@ -618,11 +619,17 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let modified = ArchiveTimestamp::from_file(path).map_err(Error::CacheRead)?; // Attempt to read the archive pointer from the cache. - let archive_entry = wheel_entry.with_file(format!("{}.rev", filename.stem())); - let archive = read_timestamped_archive(&archive_entry, modified)?; + let pointer_entry = wheel_entry.with_file(format!("{}.rev", filename.stem())); + let pointer = LocalArchivePointer::read_from(&pointer_entry)?; + + // Extract the archive from the pointer. + let archive = pointer + .filter(|pointer| pointer.is_up_to_date(modified)) + .map(LocalArchivePointer::into_archive) + .filter(|archive| archive.has_digests(hashes)); // If the file is already unzipped, and the cache is up-to-date, return it. - if let Some(archive) = archive.filter(|archive| archive.has_digests(hashes)) { + if let Some(archive) = archive { Ok(LocalWheel { dist: Dist::Built(dist.clone()), archive: archive.path, @@ -632,7 +639,13 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> } else if hashes.is_none() { // Otherwise, unzip the wheel. let archive = Archive::new(self.unzip_wheel(path, wheel_entry.path()).await?, vec![]); - write_timestamped_archive(&archive_entry, archive.clone(), modified).await?; + + // Write the archive pointer to the cache. + let pointer = LocalArchivePointer { + timestamp: modified.timestamp(), + archive: archive.clone(), + }; + pointer.write_to(&pointer_entry).await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), @@ -669,9 +682,15 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> let hashes = hashers.into_iter().map(HashDigest::from).collect(); - // Write the archive pointer to the cache. + // Create an archive. let archive = Archive::new(archive, hashes); - write_timestamped_archive(&archive_entry, archive.clone(), modified).await?; + + // Write the archive pointer to the cache. + let pointer = LocalArchivePointer { + timestamp: modified.timestamp(), + archive: archive.clone(), + }; + pointer.write_to(&pointer_entry).await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), @@ -728,37 +747,67 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> } } -/// Write a timestamped archive path to the cache. -async fn write_timestamped_archive( - cache_entry: &CacheEntry, - data: Archive, - modified: ArchiveTimestamp, -) -> Result<(), Error> { - write_atomic( - cache_entry.path(), - rmp_serde::to_vec(&CachedByTimestamp { - timestamp: modified.timestamp(), - data, - })?, - ) - .await - .map_err(Error::CacheWrite) +/// A pointer to an archive in the cache, fetched from an HTTP archive. +/// +/// Encoded with `MsgPack`, and represented on disk by a `.http` file. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct HttpArchivePointer { + archive: Archive, } -/// Read an existing timestamped archive path, if it exists and is up-to-date. -pub fn read_timestamped_archive( - cache_entry: &CacheEntry, - modified: ArchiveTimestamp, -) -> Result, Error> { - match fs_err::read(cache_entry.path()) { - Ok(cached) => { - let cached = rmp_serde::from_slice::>(&cached)?; - if cached.timestamp == modified.timestamp() { - return Ok(Some(cached.data)); +impl HttpArchivePointer { + /// Read an [`HttpArchivePointer`] from the cache. + pub fn read_from(path: impl AsRef) -> Result, Error> { + match fs_err::File::open(path.as_ref()) { + Ok(file) => { + let data = DataWithCachePolicy::from_reader(file)?.data; + let archive = rmp_serde::from_slice::(&data)?; + Ok(Some(Self { archive })) } + Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(Error::CacheRead(err)), } - Err(err) if err.kind() == io::ErrorKind::NotFound => {} - Err(err) => return Err(Error::CacheRead(err)), } - Ok(None) + + /// Return the [`Archive`] from the pointer. + pub fn into_archive(self) -> Archive { + self.archive + } +} + +/// A pointer to an archive in the cache, fetched from a local path. +/// +/// Encoded with `MsgPack`, and represented on disk by a `.rev` file. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct LocalArchivePointer { + timestamp: Timestamp, + archive: Archive, +} + +impl LocalArchivePointer { + /// Read an [`LocalArchivePointer`] from the cache. + pub fn read_from(path: impl AsRef) -> Result, Error> { + match fs_err::read(path) { + Ok(cached) => Ok(Some(rmp_serde::from_slice::(&cached)?)), + Err(err) if err.kind() == io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(Error::CacheRead(err)), + } + } + + /// Write an [`LocalArchivePointer`] to the cache. + pub async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> { + write_atomic(entry.path(), rmp_serde::to_vec(&self)?) + .await + .map_err(Error::CacheWrite) + } + + /// Returns `true` if the archive is up-to-date with the given modified timestamp. + pub fn is_up_to_date(&self, modified: ArchiveTimestamp) -> bool { + self.timestamp == modified.timestamp() + } + + /// Return the [`Archive`] from the pointer. + pub fn into_archive(self) -> Archive { + self.archive + } } diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index c856bfdc2..16f44ff20 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -7,7 +7,7 @@ use uv_fs::symlinks; use uv_types::HashStrategy; use crate::index::cached_wheel::CachedWheel; -use crate::source::{read_http_revision, read_timestamped_revision, REVISION}; +use crate::source::{HttpRevisionPointer, LocalRevisionPointer, HTTP_REVISION, LOCAL_REVISION}; use crate::Error; /// A local index of built distributions for a specific source distribution. @@ -40,12 +40,13 @@ impl<'a> BuiltWheelIndex<'a> { ); // Read the revision from the cache. - let revision_entry = cache_shard.entry(REVISION); - let Some(revision) = read_http_revision(&revision_entry)? else { + let Some(pointer) = HttpRevisionPointer::read_from(cache_shard.entry(HTTP_REVISION))? + else { return Ok(None); }; // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. + let revision = pointer.into_revision(); if !revision.satisfies(self.hasher.get(&source_dist.name)) { return Ok(None); } @@ -60,6 +61,12 @@ impl<'a> BuiltWheelIndex<'a> { WheelCache::Path(&source_dist.url).root(), ); + // Read the revision from the cache. + let Some(pointer) = LocalRevisionPointer::read_from(cache_shard.entry(LOCAL_REVISION))? + else { + return Ok(None); + }; + // Determine the last-modified time of the source distribution. let Some(modified) = ArchiveTimestamp::from_path(&source_dist.path).map_err(Error::CacheRead)? @@ -67,13 +74,13 @@ impl<'a> BuiltWheelIndex<'a> { return Err(Error::DirWithoutEntrypoint); }; - // Read the revision from the cache. - let revision_entry = cache_shard.entry(REVISION); - let Some(revision) = read_timestamped_revision(&revision_entry, modified)? else { + // If the distribution is stale, omit it from the index. + if !pointer.is_up_to_date(modified) { return Ok(None); - }; + } // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. + let revision = pointer.into_revision(); if !revision.satisfies(self.hasher.get(&source_dist.name)) { return Ok(None); } diff --git a/crates/uv-distribution/src/index/cached_wheel.rs b/crates/uv-distribution/src/index/cached_wheel.rs index 157a4cffa..7423d30cb 100644 --- a/crates/uv-distribution/src/index/cached_wheel.rs +++ b/crates/uv-distribution/src/index/cached_wheel.rs @@ -4,10 +4,9 @@ use distribution_filename::WheelFilename; use distribution_types::{CachedDirectUrlDist, CachedRegistryDist, Hashed}; use pep508_rs::VerbatimUrl; use pypi_types::HashDigest; -use uv_cache::{CacheEntry, CachedByTimestamp}; -use uv_client::DataWithCachePolicy; +use uv_cache::CacheEntry; -use crate::archive::Archive; +use crate::{HttpArchivePointer, LocalArchivePointer}; #[derive(Debug, Clone)] pub struct CachedWheel { @@ -61,9 +60,8 @@ impl CachedWheel { let filename = WheelFilename::from_stem(filename).ok()?; // Read the pointer. - let file = fs_err::File::open(path).ok()?; - let data = DataWithCachePolicy::from_reader(file).ok()?.data; - let archive = rmp_serde::from_slice::(&data).ok()?; + let pointer = HttpArchivePointer::read_from(path).ok()??; + let archive = pointer.into_archive(); // Convert to a cached wheel. let entry = CacheEntry::from_path(archive.path); @@ -76,16 +74,14 @@ impl CachedWheel { } /// Read a cached wheel from a `.rev` pointer (e.g., `anyio-4.0.0-py3-none-any.rev`). - pub fn from_revision_pointer(path: &Path) -> Option { + pub fn from_local_pointer(path: &Path) -> Option { // Determine the wheel filename. let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_stem(filename).ok()?; // Read the pointer. - let cached = fs_err::read(path).ok()?; - let archive = rmp_serde::from_slice::>(&cached) - .ok()? - .data; + let pointer = LocalArchivePointer::read_from(path).ok()??; + let archive = pointer.into_archive(); // Convert to a cached wheel. let entry = CacheEntry::from_path(archive.path); diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index da184b20f..10043920d 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -13,7 +13,7 @@ use uv_normalize::PackageName; use uv_types::HashStrategy; use crate::index::cached_wheel::CachedWheel; -use crate::source::{read_http_revision, REVISION}; +use crate::source::{HttpRevisionPointer, HTTP_REVISION}; /// A local index of distributions that originate from a registry, like `PyPI`. #[derive(Debug)] @@ -128,8 +128,7 @@ impl<'a> RegistryWheelIndex<'a> { .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("rev")) { - if let Some(wheel) = CachedWheel::from_revision_pointer(&wheel_dir.join(&file)) - { + if let Some(wheel) = CachedWheel::from_local_pointer(&wheel_dir.join(&file)) { // Enforce hash-checking based on the built distribution. if wheel.satisfies(hasher.get(package)) { Self::add_wheel(wheel, tags, &mut versions); @@ -149,9 +148,10 @@ impl<'a> RegistryWheelIndex<'a> { for shard in directories(&cache_shard) { // Read the existing metadata from the cache, if it exists. let cache_shard = cache_shard.shard(shard); - let revision_entry = cache_shard.entry(REVISION); - if let Ok(Some(revision)) = read_http_revision(&revision_entry) { + let revision_entry = cache_shard.entry(HTTP_REVISION); + if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(&revision_entry) { // Enforce hash-checking based on the source distribution. + let revision = pointer.into_revision(); if revision.satisfies(hasher.get(package)) { for wheel_dir in symlinks(cache_shard.join(revision.id())) { if let Some(wheel) = CachedWheel::from_built_source(&wheel_dir) { diff --git a/crates/uv-distribution/src/lib.rs b/crates/uv-distribution/src/lib.rs index 5245cc265..cddb2fea7 100644 --- a/crates/uv-distribution/src/lib.rs +++ b/crates/uv-distribution/src/lib.rs @@ -1,5 +1,5 @@ pub use archive::Archive; -pub use distribution_database::{read_timestamped_archive, DistributionDatabase}; +pub use distribution_database::{DistributionDatabase, HttpArchivePointer, LocalArchivePointer}; pub use download::LocalWheel; pub use error::Error; pub use git::{is_same_reference, to_precise}; diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 91172f821..78ffb00c4 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -23,7 +23,8 @@ use install_wheel_rs::metadata::read_archive_metadata; use platform_tags::Tags; use pypi_types::{HashDigest, Metadata23}; use uv_cache::{ - ArchiveTimestamp, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Freshness, WheelCache, + ArchiveTimestamp, CacheBucket, CacheEntry, CacheShard, CachedByTimestamp, Freshness, Timestamp, + WheelCache, }; use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, @@ -49,8 +50,11 @@ pub struct SourceDistributionBuilder<'a, T: BuildContext> { reporter: Option>, } -/// The name of the file that contains the revision ID, encoded via `MsgPack`. -pub(crate) const REVISION: &str = "revision.msgpack"; +/// The name of the file that contains the revision ID for a remote distribution, encoded via `MsgPack`. +pub(crate) const HTTP_REVISION: &str = "revision.http"; + +/// The name of the file that contains the revision ID for a local distribution, encoded via `MsgPack`. +pub(crate) const LOCAL_REVISION: &str = "revision.rev"; /// The name of the file that contains the cached distribution metadata, encoded via `MsgPack`. pub(crate) const METADATA: &str = "metadata.msgpack"; @@ -509,7 +513,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { cache_shard: &CacheShard, hashes: HashPolicy<'_>, ) -> Result { - let cache_entry = cache_shard.entry(REVISION); + let cache_entry = cache_shard.entry(HTTP_REVISION); let cache_control = match self.client.connectivity() { Connectivity::Online => CacheControl::from( self.build_context @@ -738,13 +742,16 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { let modified = ArchiveTimestamp::from_file(&resource.path).map_err(Error::CacheRead)?; // Read the existing metadata from the cache. - let revision_entry = cache_shard.entry(REVISION); + let revision_entry = cache_shard.entry(LOCAL_REVISION); // If the revision already exists, return it. There's no need to check for freshness, since // we use an exact timestamp. - if let Some(revision) = read_timestamped_revision(&revision_entry, modified)? { - if revision.has_digests(hashes) { - return Ok(revision); + if let Some(pointer) = LocalRevisionPointer::read_from(&revision_entry)? { + if pointer.is_up_to_date(modified) { + let revision = pointer.into_revision(); + if revision.has_digests(hashes) { + return Ok(revision); + } } } @@ -929,14 +936,31 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { }; // Read the existing metadata from the cache. - let revision_entry = cache_shard.entry(REVISION); - let revision_freshness = self + let entry = cache_shard.entry(LOCAL_REVISION); + let freshness = self .build_context .cache() - .freshness(&revision_entry, source.name()) + .freshness(&entry, source.name()) .map_err(Error::CacheRead)?; - refresh_timestamped_revision(&revision_entry, revision_freshness, modified).await + // If the revision is fresh, return it. + if freshness.is_fresh() { + if let Some(pointer) = LocalRevisionPointer::read_from(&entry)? { + if pointer.timestamp == modified.timestamp() { + return Ok(pointer.into_revision()); + } + } + } + + // Otherwise, we need to create a new revision. + let revision = Revision::new(); + let pointer = LocalRevisionPointer { + timestamp: modified.timestamp(), + revision: revision.clone(), + }; + pointer.write_to(&entry).await?; + + Ok(revision) } /// Build a source distribution from a Git repository. @@ -1418,37 +1442,74 @@ fn validate(source: &BuildableSource<'_>, metadata: &Metadata23) -> Result<(), E Ok(()) } -/// Read an existing HTTP-cached [`Revision`], if it exists. -pub(crate) fn read_http_revision(cache_entry: &CacheEntry) -> Result, Error> { - match fs_err::File::open(cache_entry.path()) { - Ok(file) => { - let data = DataWithCachePolicy::from_reader(file)?.data; - Ok(Some(rmp_serde::from_slice::(&data)?)) +/// A pointer to a source distribution revision in the cache, fetched from an HTTP archive. +/// +/// Encoded with `MsgPack`, and represented on disk by a `.http` file. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct HttpRevisionPointer { + revision: Revision, +} + +impl HttpRevisionPointer { + /// Read an [`HttpRevisionPointer`] from the cache. + pub(crate) fn read_from(path: impl AsRef) -> Result, Error> { + match fs_err::File::open(path.as_ref()) { + Ok(file) => { + let data = DataWithCachePolicy::from_reader(file)?.data; + let revision = rmp_serde::from_slice::(&data)?; + Ok(Some(Self { revision })) + } + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(Error::CacheRead(err)), } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), - Err(err) => Err(Error::CacheRead(err)), + } + + /// Return the [`Revision`] from the pointer. + pub(crate) fn into_revision(self) -> Revision { + self.revision } } -/// Read an existing timestamped [`Revision`], if it exists and is up-to-date. +/// A pointer to a source distribution revision in the cache, fetched from a local path. /// -/// If the cache entry is stale, a new entry will be created. -pub(crate) fn read_timestamped_revision( - cache_entry: &CacheEntry, - modified: ArchiveTimestamp, -) -> Result, Error> { - // If the cache entry is up-to-date, return it. - match fs_err::read(cache_entry.path()) { - Ok(cached) => { - let cached = rmp_serde::from_slice::>(&cached)?; - if cached.timestamp == modified.timestamp() { - return Ok(Some(cached.data)); - } +/// Encoded with `MsgPack`, and represented on disk by a `.rev` file. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct LocalRevisionPointer { + timestamp: Timestamp, + revision: Revision, +} + +impl LocalRevisionPointer { + /// Read an [`LocalRevisionPointer`] from the cache. + pub(crate) fn read_from(path: impl AsRef) -> Result, Error> { + match fs_err::read(path) { + Ok(cached) => Ok(Some(rmp_serde::from_slice::( + &cached, + )?)), + Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(None), + Err(err) => Err(Error::CacheRead(err)), } - Err(err) if err.kind() == std::io::ErrorKind::NotFound => {} - Err(err) => return Err(Error::CacheRead(err)), } - Ok(None) + + /// Write an [`LocalRevisionPointer`] to the cache. + async fn write_to(&self, entry: &CacheEntry) -> Result<(), Error> { + fs::create_dir_all(&entry.dir()) + .await + .map_err(Error::CacheWrite)?; + write_atomic(entry.path(), rmp_serde::to_vec(&self)?) + .await + .map_err(Error::CacheWrite) + } + + /// Returns `true` if the revision is up-to-date with the given modified timestamp. + pub(crate) fn is_up_to_date(&self, modified: ArchiveTimestamp) -> bool { + self.timestamp == modified.timestamp() + } + + /// Return the [`Revision`] from the pointer. + pub(crate) fn into_revision(self) -> Revision { + self.revision + } } /// Read the [`Metadata23`] from a source distribution's `PKG-INFO` file, if it uses Metadata 2.2 @@ -1503,38 +1564,6 @@ async fn read_pyproject_toml( Ok(metadata) } -/// Read an existing timestamped [`Manifest`], if it exists and is up-to-date. -/// -/// If the cache entry is stale, a new entry will be created. -async fn refresh_timestamped_revision( - cache_entry: &CacheEntry, - freshness: Freshness, - modified: ArchiveTimestamp, -) -> Result { - // If we know the exact modification time, we don't need to force a revalidate. - if matches!(modified, ArchiveTimestamp::Exact(_)) || freshness.is_fresh() { - if let Some(revision) = read_timestamped_revision(cache_entry, modified)? { - return Ok(revision); - } - } - - // Otherwise, create a new revision. - let revision = Revision::new(); - fs::create_dir_all(&cache_entry.dir()) - .await - .map_err(Error::CacheWrite)?; - write_atomic( - cache_entry.path(), - rmp_serde::to_vec(&CachedByTimestamp { - timestamp: modified.timestamp(), - data: revision.clone(), - })?, - ) - .await - .map_err(Error::CacheWrite)?; - Ok(revision) -} - /// Read an existing cached [`Metadata23`], if it exists. async fn read_cached_metadata(cache_entry: &CacheEntry) -> Result, Error> { match fs::read(&cache_entry.path()).await { diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index d9122b96f..d0b191bae 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -1,6 +1,5 @@ use std::collections::hash_map::Entry; use std::hash::BuildHasherDefault; -use std::io; use anyhow::{bail, Result}; use rustc_hash::FxHashMap; @@ -14,9 +13,11 @@ use distribution_types::{ use pep508_rs::{Requirement, VersionOrUrl}; use platform_tags::Tags; use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache, CacheBucket, WheelCache}; -use uv_client::DataWithCachePolicy; + use uv_configuration::{NoBinary, Reinstall}; -use uv_distribution::{read_timestamped_archive, Archive, BuiltWheelIndex, RegistryWheelIndex}; +use uv_distribution::{ + BuiltWheelIndex, HttpArchivePointer, LocalArchivePointer, RegistryWheelIndex, +}; use uv_fs::Simplified; use uv_interpreter::PythonEnvironment; use uv_types::HashStrategy; @@ -256,31 +257,20 @@ impl<'a> Planner<'a> { .entry(format!("{}.http", wheel.filename.stem())); // Read the HTTP pointer. - match fs_err::File::open(cache_entry.path()) { - Ok(file) => { - let data = DataWithCachePolicy::from_reader(file)?.data; - let archive = rmp_serde::from_slice::(&data)?; + if let Some(pointer) = HttpArchivePointer::read_from(&cache_entry)? { + let archive = pointer.into_archive(); + if archive.satisfies(hasher.get(&requirement.name)) { + let cached_dist = CachedDirectUrlDist::from_url( + wheel.filename, + wheel.url, + archive.hashes, + archive.path, + ); - // Enforce hash checking. - if archive.satisfies(hasher.get(&requirement.name)) { - let cached_dist = CachedDirectUrlDist::from_url( - wheel.filename, - wheel.url, - archive.hashes, - archive.path, - ); - - debug!( - "URL wheel requirement already cached: {cached_dist}" - ); - cached.push(CachedDist::Url(cached_dist)); - continue; - } + debug!("URL wheel requirement already cached: {cached_dist}"); + cached.push(CachedDist::Url(cached_dist)); + continue; } - Err(err) if err.kind() == io::ErrorKind::NotFound => { - // The cache entry doesn't exist, so it's not fresh. - } - Err(err) => return Err(err.into()), } } Dist::Built(BuiltDist::Path(wheel)) => { @@ -307,21 +297,24 @@ impl<'a> Planner<'a> { ) .entry(format!("{}.rev", wheel.filename.stem())); - if let Some(archive) = read_timestamped_archive( - &cache_entry, - ArchiveTimestamp::from_file(&wheel.path)?, - )? { - if archive.satisfies(hasher.get(&requirement.name)) { - let cached_dist = CachedDirectUrlDist::from_url( - wheel.filename, - wheel.url, - archive.hashes, - archive.path, - ); + if let Some(pointer) = LocalArchivePointer::read_from(&cache_entry)? { + let timestamp = ArchiveTimestamp::from_file(&wheel.path)?; + if pointer.is_up_to_date(timestamp) { + let archive = pointer.into_archive(); + if archive.satisfies(hasher.get(&requirement.name)) { + let cached_dist = CachedDirectUrlDist::from_url( + wheel.filename, + wheel.url, + archive.hashes, + archive.path, + ); - debug!("Path wheel requirement already cached: {cached_dist}"); - cached.push(CachedDist::Url(cached_dist)); - continue; + debug!( + "Path wheel requirement already cached: {cached_dist}" + ); + cached.push(CachedDist::Url(cached_dist)); + continue; + } } } } From c294c7098f1b2fc228c30e720702e927eb1a9e87 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 18:00:51 -0400 Subject: [PATCH 096/110] Remove unnecessary `touch` calls from tests (#2981) You only need to `touch` if you don't end up writing to the file. --- crates/uv/tests/pip_check.rs | 7 +---- crates/uv/tests/pip_install.rs | 18 ------------- crates/uv/tests/pip_list.rs | 3 +-- crates/uv/tests/pip_show.rs | 10 +------ crates/uv/tests/pip_sync.rs | 45 +------------------------------- crates/uv/tests/pip_uninstall.rs | 8 ------ 6 files changed, 4 insertions(+), 87 deletions(-) diff --git a/crates/uv/tests/pip_check.rs b/crates/uv/tests/pip_check.rs index 61503a019..28e2aecdd 100644 --- a/crates/uv/tests/pip_check.rs +++ b/crates/uv/tests/pip_check.rs @@ -1,8 +1,8 @@ use std::process::Command; use anyhow::Result; +use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; -use assert_fs::fixture::{FileTouch, FileWriteStr}; use common::uv_snapshot; @@ -53,7 +53,6 @@ fn check_compatible_packages() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("requests==2.31.0")?; uv_snapshot!(install_command(&context) @@ -97,7 +96,6 @@ fn check_incompatible_packages() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("requests==2.31.0")?; uv_snapshot!(install_command(&context) @@ -121,7 +119,6 @@ fn check_incompatible_packages() -> Result<()> { ); let requirements_txt_idna = context.temp_dir.child("requirements_idna.txt"); - requirements_txt_idna.touch()?; requirements_txt_idna.write_str("idna==2.4")?; uv_snapshot!(install_command(&context) @@ -165,7 +162,6 @@ fn check_multiple_incompatible_packages() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("requests==2.31.0")?; uv_snapshot!(install_command(&context) @@ -189,7 +185,6 @@ fn check_multiple_incompatible_packages() -> Result<()> { ); let requirements_txt_two = context.temp_dir.child("requirements_two.txt"); - requirements_txt_two.touch()?; requirements_txt_two.write_str("idna==2.4\nurllib3==1.20")?; uv_snapshot!(install_command(&context) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index b2f0b7b5b..055d84f36 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -413,7 +413,6 @@ fn respect_installed_and_reinstall() -> Result<()> { // Install Flask. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("Flask==2.3.2")?; uv_snapshot!(context.install() @@ -442,7 +441,6 @@ fn respect_installed_and_reinstall() -> Result<()> { // Re-install Flask. We should respect the existing version. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("Flask")?; uv_snapshot!(context.install() @@ -462,7 +460,6 @@ fn respect_installed_and_reinstall() -> Result<()> { // Install a newer version of Flask. We should upgrade it. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("Flask==2.3.3")?; let filters = if cfg!(windows) { @@ -494,7 +491,6 @@ fn respect_installed_and_reinstall() -> Result<()> { // Re-install Flask. We should upgrade it. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("Flask")?; uv_snapshot!(filters, context.install() @@ -518,7 +514,6 @@ fn respect_installed_and_reinstall() -> Result<()> { // Re-install Flask. We should install even though the version is current let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("Flask")?; uv_snapshot!(filters, context.install() @@ -577,7 +572,6 @@ fn reinstall_extras() -> Result<()> { // Re-install httpx, with an extra. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("httpx[http2]")?; uv_snapshot!(context.install() @@ -610,7 +604,6 @@ fn reinstall_incomplete() -> Result<()> { // Install anyio. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("anyio==3.7.0")?; uv_snapshot!(context.install() @@ -635,7 +628,6 @@ fn reinstall_incomplete() -> Result<()> { // Re-install anyio. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("anyio==4.0.0")?; uv_snapshot!(context.filters(), context.install() @@ -665,7 +657,6 @@ fn allow_incompatibilities() -> Result<()> { // Install Flask, which relies on `Werkzeug>=3.0.0`. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("Flask")?; uv_snapshot!(context.install() @@ -694,7 +685,6 @@ fn allow_incompatibilities() -> Result<()> { // Install an incompatible version of Jinja2. let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("jinja2==2.11.3")?; uv_snapshot!(context.install() @@ -2508,7 +2498,6 @@ fn no_build_isolation() -> Result<()> { fn install_utf16le_requirements() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_binary(&utf8_to_utf16_with_bom_le("tomli"))?; uv_snapshot!(context.install_without_exclude_newer() @@ -2535,7 +2524,6 @@ fn install_utf16le_requirements() -> Result<()> { fn install_utf16be_requirements() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_binary(&utf8_to_utf16_with_bom_be("tomli"))?; uv_snapshot!(context.install_without_exclude_newer() @@ -2579,7 +2567,6 @@ fn utf8_to_utf16_with_bom_be(s: &str) -> Vec { fn dry_run_install() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("httpx==0.25.1")?; uv_snapshot!(context.install() @@ -2612,7 +2599,6 @@ fn dry_run_install() -> std::result::Result<(), Box> { fn dry_run_install_url_dependency() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?; uv_snapshot!(context.install() @@ -2641,7 +2627,6 @@ fn dry_run_install_url_dependency() -> std::result::Result<(), Box std::result::Result<(), Box> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("anyio @ https://files.pythonhosted.org/packages/2d/b8/7333d87d5f03247215d86a86362fd3e324111788c6cdd8d2e6196a6ba833/anyio-4.2.0.tar.gz")?; // Install the URL dependency @@ -2693,7 +2678,6 @@ fn dry_run_uninstall_url_dependency() -> std::result::Result<(), Box std::result::Result<(), Box> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("httpx==0.25.1")?; // Install the package @@ -2744,7 +2728,6 @@ fn dry_run_install_transitive_dependency_already_installed( let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("httpcore==1.0.2")?; // Install a dependency of httpx @@ -2795,7 +2778,6 @@ fn dry_run_install_transitive_dependency_already_installed( fn dry_run_install_then_upgrade() -> std::result::Result<(), Box> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("httpx==0.25.0")?; // Install the package diff --git a/crates/uv/tests/pip_list.rs b/crates/uv/tests/pip_list.rs index e738f96c6..4b4fb7b36 100644 --- a/crates/uv/tests/pip_list.rs +++ b/crates/uv/tests/pip_list.rs @@ -1,8 +1,8 @@ use std::process::Command; use anyhow::Result; +use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; -use assert_fs::fixture::{FileTouch, FileWriteStr}; use common::uv_snapshot; @@ -59,7 +59,6 @@ fn list_single_no_editable() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; uv_snapshot!(install_command(&context) diff --git a/crates/uv/tests/pip_show.rs b/crates/uv/tests/pip_show.rs index 2d3cc984e..4bfbbce15 100644 --- a/crates/uv/tests/pip_show.rs +++ b/crates/uv/tests/pip_show.rs @@ -3,8 +3,8 @@ use std::process::Command; use anyhow::Result; use assert_cmd::prelude::*; +use assert_fs::fixture::FileWriteStr; use assert_fs::fixture::PathChild; -use assert_fs::fixture::{FileTouch, FileWriteStr}; use indoc::indoc; use common::uv_snapshot; @@ -63,7 +63,6 @@ fn show_requires_multiple() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("requests==2.31.0")?; uv_snapshot!(install_command(&context) @@ -119,7 +118,6 @@ fn show_python_version_marker() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("click==8.1.7")?; uv_snapshot!(install_command(&context) @@ -175,7 +173,6 @@ fn show_found_single_package() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; uv_snapshot!(install_command(&context) @@ -226,7 +223,6 @@ fn show_found_multiple_packages() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str(indoc! {r" MarkupSafe==2.1.3 pip==21.3.1 @@ -289,7 +285,6 @@ fn show_found_one_out_of_three() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str(indoc! {r" MarkupSafe==2.1.3 pip==21.3.1 @@ -348,7 +343,6 @@ fn show_found_one_out_of_two_quiet() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str(indoc! {r" MarkupSafe==2.1.3 pip==21.3.1 @@ -402,7 +396,6 @@ fn show_empty_quiet() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str(indoc! {r" MarkupSafe==2.1.3 pip==21.3.1 @@ -497,7 +490,6 @@ fn show_required_by_multiple() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str(indoc! {r" anyio==4.0.0 requests==2.31.0 diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index bc07eafc7..37365b8a2 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -142,7 +142,6 @@ fn install() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; uv_snapshot!(command(&context) @@ -184,7 +183,6 @@ fn install_copy() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; uv_snapshot!(command(&context) @@ -220,7 +218,6 @@ fn install_hardlink() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; uv_snapshot!(command(&context) @@ -256,7 +253,6 @@ fn install_many() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; uv_snapshot!(command(&context) @@ -288,7 +284,6 @@ fn noop() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; command(&context) @@ -322,7 +317,6 @@ fn link() -> Result<()> { let venv1 = &context.venv; let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; Command::new(get_bin()) @@ -384,7 +378,6 @@ fn add_remove() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; command(&context) @@ -394,7 +387,6 @@ fn add_remove() -> Result<()> { .success(); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("tomli==2.0.1")?; uv_snapshot!(command(&context) @@ -427,7 +419,6 @@ fn install_sequential() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; command(&context) @@ -437,7 +428,6 @@ fn install_sequential() -> Result<()> { .success(); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; uv_snapshot!(command(&context) @@ -469,7 +459,6 @@ fn upgrade() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("tomli==2.0.0")?; command(&context) @@ -479,7 +468,6 @@ fn upgrade() -> Result<()> { .success(); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("tomli==2.0.1")?; uv_snapshot!(command(&context) @@ -510,7 +498,6 @@ fn install_url() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl")?; uv_snapshot!(command(&context) @@ -540,7 +527,6 @@ fn install_git_commit() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74")?; uv_snapshot!(command(&context) @@ -570,7 +556,6 @@ fn install_git_tag() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug @ git+https://github.com/pallets/WerkZeug.git@2.0.0")?; uv_snapshot!(command(&context) @@ -600,7 +585,6 @@ fn install_git_subdirectories() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("example-pkg-a @ git+https://github.com/pypa/sample-namespace-packages.git@df7530eeb8fa0cb7dbb8ecb28363e8e36bfa2f45#subdirectory=pkg_resources/pkg_a\nexample-pkg-b @ git+https://github.com/pypa/sample-namespace-packages.git@df7530eeb8fa0cb7dbb8ecb28363e8e36bfa2f45#subdirectory=pkg_resources/pkg_b")?; uv_snapshot!(command(&context) @@ -632,7 +616,6 @@ fn install_sdist() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("Werkzeug==0.9.6")?; uv_snapshot!(command(&context) @@ -661,7 +644,6 @@ fn install_sdist_url() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("Werkzeug @ https://files.pythonhosted.org/packages/63/69/5702e5eb897d1a144001e21d676676bcb87b88c0862f947509ea95ea54fc/Werkzeug-0.9.6.tar.gz")?; uv_snapshot!(command(&context) @@ -691,7 +673,6 @@ fn install_url_then_install_url() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl")?; command(&context) @@ -724,7 +705,6 @@ fn install_url_then_install_version() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl")?; command(&context) @@ -734,7 +714,6 @@ fn install_url_then_install_version() -> Result<()> { .success(); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug==2.0.0")?; uv_snapshot!(command(&context) @@ -761,7 +740,6 @@ fn install_version_then_install_url() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug==2.0.0")?; command(&context) @@ -771,7 +749,6 @@ fn install_version_then_install_url() -> Result<()> { .success(); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug @ https://files.pythonhosted.org/packages/ff/1d/960bb4017c68674a1cb099534840f18d3def3ce44aed12b5ed8b78e0153e/Werkzeug-2.0.0-py3-none-any.whl")?; uv_snapshot!(command(&context) @@ -803,7 +780,6 @@ fn install_numpy_py38() -> Result<()> { let context = TestContext::new("3.8"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("numpy")?; uv_snapshot!(command(&context) @@ -832,7 +808,6 @@ fn install_no_index() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; uv_snapshot!(command(&context) @@ -863,7 +838,6 @@ fn install_no_index_cached() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; uv_snapshot!(command(&context) @@ -913,10 +887,8 @@ fn install_no_index_cached() -> Result<()> { fn warn_on_yanked_version() -> Result<()> { let context = TestContext::new("3.12"); - let requirements_in = context.temp_dir.child("requirements.txt"); - requirements_in.touch()?; - // This version is yanked. + let requirements_in = context.temp_dir.child("requirements.txt"); requirements_in.write_str("colorama==0.4.2")?; uv_snapshot!(context.filters(), windows_filters=false, command(&context) @@ -1167,7 +1139,6 @@ fn install_ujson() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("ujson @ https://files.pythonhosted.org/packages/43/1a/b0a027144aa5c8f4ea654f4afdd634578b450807bb70b9f8bad00d6f6d3c/ujson-5.7.0.tar.gz")?; uv_snapshot!(command(&context) @@ -1207,7 +1178,6 @@ fn install_build_system_no_backend() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("build-system-no-backend @ https://files.pythonhosted.org/packages/ec/25/1e531108ca027dc3a3b37d351f4b86d811df4884c6a81cd99e73b8b589f5/build-system-no-backend-0.1.0.tar.gz")?; uv_snapshot!(command(&context) @@ -1238,7 +1208,6 @@ fn install_url_source_dist_cached() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("tqdm @ https://files.pythonhosted.org/packages/62/06/d5604a70d160f6a6ca5fd2ba25597c24abd5c5ca5f437263d177ac242308/tqdm-4.66.1.tar.gz")?; let filters = if cfg!(windows) { @@ -1336,7 +1305,6 @@ fn install_git_source_dist_cached() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74")?; uv_snapshot!(command(&context) @@ -1433,7 +1401,6 @@ fn install_registry_source_dist_cached() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("future==0.18.3")?; uv_snapshot!(command(&context) @@ -1742,7 +1709,6 @@ fn install_url_built_dist_cached() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("tqdm @ https://files.pythonhosted.org/packages/00/e5/f12a80907d0884e6dff9c16d0c0114d81b8cd07dc3ae54c5e962cc83037e/tqdm-4.66.1-py3-none-any.whl")?; let filters = if cfg!(windows) { @@ -1839,7 +1805,6 @@ fn duplicate_package_overlap() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3\nMarkupSafe==2.1.2")?; uv_snapshot!(command(&context) @@ -1864,7 +1829,6 @@ fn duplicate_package_disjoint() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3\nMarkupSafe==2.1.2 ; python_version < '3.6'")?; uv_snapshot!(command(&context) @@ -1891,7 +1855,6 @@ fn reinstall() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; uv_snapshot!(command(&context) @@ -1944,7 +1907,6 @@ fn reinstall_package() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; uv_snapshot!(command(&context) @@ -1997,7 +1959,6 @@ fn reinstall_git() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("werkzeug @ git+https://github.com/pallets/werkzeug.git@af160e0b6b7ddd81c22f1652c728ff5ac72d5c74")?; uv_snapshot!(command(&context) @@ -2046,7 +2007,6 @@ fn refresh() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; uv_snapshot!(command(&context) @@ -2104,7 +2064,6 @@ fn refresh_package() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3\ntomli==2.0.1")?; uv_snapshot!(command(&context) @@ -2787,7 +2746,6 @@ fn pip_entrypoints() -> Result<()> { // TODO(konstin): Remove git dep when the next pip version is released. for pip_requirement in ["pip==24.0", "pip @ git+https://github.com/pypa/pip"] { let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str(pip_requirement)?; command(&context) @@ -2899,7 +2857,6 @@ fn compile() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; uv_snapshot!(command(&context) diff --git a/crates/uv/tests/pip_uninstall.rs b/crates/uv/tests/pip_uninstall.rs index 84349b184..b6808afb9 100644 --- a/crates/uv/tests/pip_uninstall.rs +++ b/crates/uv/tests/pip_uninstall.rs @@ -127,7 +127,6 @@ fn missing_requirements_txt() -> Result<()> { fn invalid_requirements_txt_requirement() -> Result<()> { let temp_dir = assert_fs::TempDir::new()?; let requirements_txt = temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("flask==1.0.x")?; uv_snapshot!(Command::new(get_bin()) @@ -155,7 +154,6 @@ fn uninstall() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; sync_command(&context) @@ -197,7 +195,6 @@ fn missing_record() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("MarkupSafe==2.1.3")?; sync_command(&context) @@ -235,7 +232,6 @@ fn uninstall_editable_by_name() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str(&format!( "-e {}", context @@ -283,7 +279,6 @@ fn uninstall_by_path() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str( context .workspace_root @@ -331,7 +326,6 @@ fn uninstall_duplicate_by_path() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str( context .workspace_root @@ -383,7 +377,6 @@ fn uninstall_duplicate() -> Result<()> { // Sync a version of `pip` into a virtual environment. let context1 = TestContext::new("3.12"); let requirements_txt = context1.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("pip==21.3.1")?; // Run `pip sync`. @@ -395,7 +388,6 @@ fn uninstall_duplicate() -> Result<()> { // Sync a different version of `pip` into a virtual environment. let context2 = TestContext::new("3.12"); let requirements_txt = context2.temp_dir.child("requirements.txt"); - requirements_txt.touch()?; requirements_txt.write_str("pip==22.1.1")?; // Run `pip sync`. From 32f129c24547c9fbcd9faf0c554ef6232947c1b1 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 21:07:51 -0400 Subject: [PATCH 097/110] Store IDs rather than paths in the cache (#2985) ## Summary Similar to `Revision`, we now store IDs in the `Archive` entires rather than absolute paths. This makes the cache robust to moves, etc. Closes https://github.com/astral-sh/uv/issues/2908. --- crates/uv-cache/src/archive.rs | 24 +++++++++ crates/uv-cache/src/lib.rs | 17 ++++-- crates/uv-distribution/src/archive.rs | 23 +++----- .../src/distribution_database.rs | 53 ++++++++++--------- .../uv-distribution/src/index/cached_wheel.rs | 18 +++---- .../src/index/registry_wheel_index.rs | 8 ++- crates/uv-distribution/src/source/revision.rs | 24 +++++++-- crates/uv-installer/src/plan.rs | 4 +- 8 files changed, 107 insertions(+), 64 deletions(-) create mode 100644 crates/uv-cache/src/archive.rs diff --git a/crates/uv-cache/src/archive.rs b/crates/uv-cache/src/archive.rs new file mode 100644 index 000000000..31243f25c --- /dev/null +++ b/crates/uv-cache/src/archive.rs @@ -0,0 +1,24 @@ +use std::path::Path; + +/// A unique identifier for an archive (unzipped wheel) in the cache. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct ArchiveId(String); + +impl Default for ArchiveId { + fn default() -> Self { + Self::new() + } +} + +impl ArchiveId { + /// Generate a new unique identifier for an archive. + pub fn new() -> Self { + Self(nanoid::nanoid!()) + } +} + +impl AsRef for ArchiveId { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} diff --git a/crates/uv-cache/src/lib.rs b/crates/uv-cache/src/lib.rs index e232d5992..5c07c3902 100644 --- a/crates/uv-cache/src/lib.rs +++ b/crates/uv-cache/src/lib.rs @@ -23,7 +23,9 @@ use crate::removal::{rm_rf, Removal}; pub use crate::timestamp::Timestamp; pub use crate::wheel::WheelCache; use crate::wheel::WheelCacheKind; +pub use archive::ArchiveId; +mod archive; mod by_timestamp; #[cfg(feature = "clap")] mod cli; @@ -173,6 +175,11 @@ impl Cache { CacheEntry::new(self.bucket(cache_bucket).join(dir), file) } + /// Return the path to an archive in the cache. + pub fn archive(&self, id: &ArchiveId) -> PathBuf { + self.bucket(CacheBucket::Archive).join(id) + } + /// Returns `true` if a cache entry must be revalidated given the [`Refresh`] policy. pub fn must_revalidate(&self, package: &PackageName) -> bool { match &self.refresh { @@ -214,18 +221,18 @@ impl Cache { } } - /// Persist a temporary directory to the artifact store. + /// Persist a temporary directory to the artifact store, returning its unique ID. pub async fn persist( &self, temp_dir: impl AsRef, path: impl AsRef, - ) -> io::Result { + ) -> io::Result { // Create a unique ID for the artifact. // TODO(charlie): Support content-addressed persistence via SHAs. - let id = nanoid::nanoid!(); + let id = ArchiveId::new(); // Move the temporary directory into the directory store. - let archive_entry = self.entry(CacheBucket::Archive, "", id); + let archive_entry = self.entry(CacheBucket::Archive, "", &id); fs_err::create_dir_all(archive_entry.dir())?; uv_fs::rename_with_retry(temp_dir.as_ref(), archive_entry.path()).await?; @@ -233,7 +240,7 @@ impl Cache { fs_err::create_dir_all(path.as_ref().parent().expect("Cache entry to have parent"))?; uv_fs::replace_symlink(archive_entry.path(), path.as_ref())?; - Ok(archive_entry.into_path_buf()) + Ok(id) } /// Initialize a directory for use as a cache. diff --git a/crates/uv-distribution/src/archive.rs b/crates/uv-distribution/src/archive.rs index a53de619d..3d02d9e85 100644 --- a/crates/uv-distribution/src/archive.rs +++ b/crates/uv-distribution/src/archive.rs @@ -1,31 +1,20 @@ -use std::path::PathBuf; - use distribution_types::Hashed; use pypi_types::HashDigest; +use uv_cache::ArchiveId; /// An archive (unzipped wheel) that exists in the local cache. #[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] pub struct Archive { - /// The path to the archive entry in the wheel's archive bucket. - pub path: PathBuf, + /// The unique ID of the entry in the wheel's archive bucket. + pub id: ArchiveId, /// The computed hashes of the archive. pub hashes: Vec, } impl Archive { - /// Create a new [`Archive`] with the given path and hashes. - pub(crate) fn new(path: PathBuf, hashes: Vec) -> Self { - Self { path, hashes } - } - - /// Return the path to the archive entry in the wheel's archive bucket. - pub fn path(&self) -> &PathBuf { - &self.path - } - - /// Return the computed hashes of the archive. - pub fn hashes(&self) -> &[HashDigest] { - &self.hashes + /// Create a new [`Archive`] with the given ID and hashes. + pub(crate) fn new(id: ArchiveId, hashes: Vec) -> Self { + Self { id, hashes } } } diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 04c941a5d..67bc80cb5 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -1,5 +1,5 @@ use std::io; -use std::path::{Path, PathBuf}; +use std::path::Path; use std::sync::Arc; use futures::{FutureExt, TryStreamExt}; @@ -16,7 +16,7 @@ use distribution_types::{ }; use platform_tags::Tags; use pypi_types::{HashDigest, Metadata23}; -use uv_cache::{ArchiveTimestamp, CacheBucket, CacheEntry, Timestamp, WheelCache}; +use uv_cache::{ArchiveId, ArchiveTimestamp, CacheBucket, CacheEntry, Timestamp, WheelCache}; use uv_client::{ CacheControl, CachedClientError, Connectivity, DataWithCachePolicy, RegistryClient, }; @@ -136,11 +136,11 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> // Unzip into the editable wheel directory. let path = editable_wheel_dir.join(&disk_filename); let target = editable_wheel_dir.join(cache_key::digest(&editable.path)); - let archive = self.unzip_wheel(&path, &target).await?; + let id = self.unzip_wheel(&path, &target).await?; let wheel = LocalWheel { dist, filename, - archive, + archive: self.build_context.cache().archive(&id), hashes: vec![], }; @@ -200,7 +200,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> { Ok(archive) => Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive: archive.path, + archive: self.build_context.cache().archive(&archive.id), hashes: archive.hashes, filename: wheel.filename.clone(), }), @@ -216,7 +216,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive: archive.path, + archive: self.build_context.cache().archive(&archive.id), hashes: archive.hashes, filename: wheel.filename.clone(), }) @@ -246,7 +246,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> { Ok(archive) => Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive: archive.path, + archive: self.build_context.cache().archive(&archive.id), hashes: archive.hashes, filename: wheel.filename.clone(), }), @@ -268,7 +268,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .await?; Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive: archive.path, + archive: self.build_context.cache().archive(&archive.id), hashes: archive.hashes, filename: wheel.filename.clone(), }) @@ -326,11 +326,13 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> } // Otherwise, unzip the wheel. + let id = self + .unzip_wheel(&built_wheel.path, &built_wheel.target) + .await?; + Ok(LocalWheel { dist: Dist::Source(dist.clone()), - archive: self - .unzip_wheel(&built_wheel.path, &built_wheel.target) - .await?, + archive: self.build_context.cache().archive(&id), hashes: built_wheel.hashes, filename: built_wheel.filename, }) @@ -442,14 +444,15 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> } // Persist the temporary directory to the directory store. - let path = self + let id = self .build_context .cache() .persist(temp_dir.into_path(), wheel_entry.path()) .await .map_err(Error::CacheRead)?; + Ok(Archive::new( - path, + id, hashers.into_iter().map(HashDigest::from).collect(), )) } @@ -557,14 +560,14 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> }; // Persist the temporary directory to the directory store. - let path = self + let id = self .build_context .cache() .persist(temp_dir.into_path(), wheel_entry.path()) .await .map_err(Error::CacheRead)?; - Ok(Archive::new(path, hashes)) + Ok(Archive::new(id, hashes)) } .instrument(info_span!("wheel", wheel = %dist)) }; @@ -632,7 +635,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> if let Some(archive) = archive { Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive: archive.path, + archive: self.build_context.cache().archive(&archive.id), hashes: archive.hashes, filename: filename.clone(), }) @@ -649,7 +652,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive: archive.path, + archive: self.build_context.cache().archive(&archive.id), hashes: archive.hashes, filename: filename.clone(), }) @@ -672,18 +675,18 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> // Exhaust the reader to compute the hash. hasher.finish().await.map_err(Error::HashExhaustion)?; + let hashes = hashers.into_iter().map(HashDigest::from).collect(); + // Persist the temporary directory to the directory store. - let archive = self + let id = self .build_context .cache() .persist(temp_dir.into_path(), wheel_entry.path()) .await .map_err(Error::CacheWrite)?; - let hashes = hashers.into_iter().map(HashDigest::from).collect(); - // Create an archive. - let archive = Archive::new(archive, hashes); + let archive = Archive::new(id, hashes); // Write the archive pointer to the cache. let pointer = LocalArchivePointer { @@ -694,7 +697,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> Ok(LocalWheel { dist: Dist::Built(dist.clone()), - archive: archive.path, + archive: self.build_context.cache().archive(&archive.id), hashes: archive.hashes, filename: filename.clone(), }) @@ -702,7 +705,7 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> } /// Unzip a wheel into the cache, returning the path to the unzipped directory. - async fn unzip_wheel(&self, path: &Path, target: &Path) -> Result { + async fn unzip_wheel(&self, path: &Path, target: &Path) -> Result { let temp_dir = tokio::task::spawn_blocking({ let path = path.to_owned(); let root = self.build_context.cache().root().to_path_buf(); @@ -716,14 +719,14 @@ impl<'a, Context: BuildContext + Send + Sync> DistributionDatabase<'a, Context> .await??; // Persist the temporary directory to the directory store. - let archive = self + let id = self .build_context .cache() .persist(temp_dir.into_path(), target) .await .map_err(Error::CacheWrite)?; - Ok(archive) + Ok(id) } /// Returns a GET [`reqwest::Request`] for the given URL. diff --git a/crates/uv-distribution/src/index/cached_wheel.rs b/crates/uv-distribution/src/index/cached_wheel.rs index 7423d30cb..eea658c8f 100644 --- a/crates/uv-distribution/src/index/cached_wheel.rs +++ b/crates/uv-distribution/src/index/cached_wheel.rs @@ -4,9 +4,9 @@ use distribution_filename::WheelFilename; use distribution_types::{CachedDirectUrlDist, CachedRegistryDist, Hashed}; use pep508_rs::VerbatimUrl; use pypi_types::HashDigest; -use uv_cache::CacheEntry; +use uv_cache::{Cache, CacheBucket, CacheEntry}; -use crate::{HttpArchivePointer, LocalArchivePointer}; +use crate::{Archive, HttpArchivePointer, LocalArchivePointer}; #[derive(Debug, Clone)] pub struct CachedWheel { @@ -54,18 +54,17 @@ impl CachedWheel { } /// Read a cached wheel from a `.http` pointer (e.g., `anyio-4.0.0-py3-none-any.http`). - pub fn from_http_pointer(path: &Path) -> Option { + pub fn from_http_pointer(path: &Path, cache: &Cache) -> Option { // Determine the wheel filename. let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_stem(filename).ok()?; // Read the pointer. let pointer = HttpArchivePointer::read_from(path).ok()??; - let archive = pointer.into_archive(); + let Archive { id, hashes } = pointer.into_archive(); // Convert to a cached wheel. - let entry = CacheEntry::from_path(archive.path); - let hashes = archive.hashes; + let entry = cache.entry(CacheBucket::Archive, "", id); Some(Self { filename, entry, @@ -74,18 +73,17 @@ impl CachedWheel { } /// Read a cached wheel from a `.rev` pointer (e.g., `anyio-4.0.0-py3-none-any.rev`). - pub fn from_local_pointer(path: &Path) -> Option { + pub fn from_local_pointer(path: &Path, cache: &Cache) -> Option { // Determine the wheel filename. let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_stem(filename).ok()?; // Read the pointer. let pointer = LocalArchivePointer::read_from(path).ok()??; - let archive = pointer.into_archive(); + let Archive { id, hashes } = pointer.into_archive(); // Convert to a cached wheel. - let entry = CacheEntry::from_path(archive.path); - let hashes = archive.hashes; + let entry = cache.entry(CacheBucket::Archive, "", id); Some(Self { filename, entry, diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 10043920d..4cf9c0594 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -116,7 +116,9 @@ impl<'a> RegistryWheelIndex<'a> { .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("http")) { - if let Some(wheel) = CachedWheel::from_http_pointer(&wheel_dir.join(&file)) { + if let Some(wheel) = + CachedWheel::from_http_pointer(&wheel_dir.join(&file), cache) + { // Enforce hash-checking based on the built distribution. if wheel.satisfies(hasher.get(package)) { Self::add_wheel(wheel, tags, &mut versions); @@ -128,7 +130,9 @@ impl<'a> RegistryWheelIndex<'a> { .extension() .is_some_and(|ext| ext.eq_ignore_ascii_case("rev")) { - if let Some(wheel) = CachedWheel::from_local_pointer(&wheel_dir.join(&file)) { + if let Some(wheel) = + CachedWheel::from_local_pointer(&wheel_dir.join(&file), cache) + { // Enforce hash-checking based on the built distribution. if wheel.satisfies(hasher.get(package)) { Self::add_wheel(wheel, tags, &mut versions); diff --git a/crates/uv-distribution/src/source/revision.rs b/crates/uv-distribution/src/source/revision.rs index aadc2945a..64cbc127b 100644 --- a/crates/uv-distribution/src/source/revision.rs +++ b/crates/uv-distribution/src/source/revision.rs @@ -1,5 +1,6 @@ use distribution_types::Hashed; use serde::{Deserialize, Serialize}; +use std::path::Path; use pypi_types::HashDigest; @@ -11,7 +12,7 @@ use pypi_types::HashDigest; /// the distribution, despite the reported version number remaining the same. #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct Revision { - id: String, + id: RevisionId, hashes: Vec, } @@ -19,13 +20,13 @@ impl Revision { /// Initialize a new [`Revision`] with a random UUID. pub(crate) fn new() -> Self { Self { - id: nanoid::nanoid!(), + id: RevisionId::new(), hashes: vec![], } } /// Return the unique ID of the manifest. - pub(crate) fn id(&self) -> &str { + pub(crate) fn id(&self) -> &RevisionId { &self.id } @@ -52,3 +53,20 @@ impl Hashed for Revision { &self.hashes } } + +/// A unique identifier for a revision of a source distribution. +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub(crate) struct RevisionId(String); + +impl RevisionId { + /// Generate a new unique identifier for an archive. + fn new() -> Self { + Self(nanoid::nanoid!()) + } +} + +impl AsRef for RevisionId { + fn as_ref(&self) -> &Path { + self.0.as_ref() + } +} diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index d0b191bae..9895ac3be 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -264,7 +264,7 @@ impl<'a> Planner<'a> { wheel.filename, wheel.url, archive.hashes, - archive.path, + cache.archive(&archive.id), ); debug!("URL wheel requirement already cached: {cached_dist}"); @@ -306,7 +306,7 @@ impl<'a> Planner<'a> { wheel.filename, wheel.url, archive.hashes, - archive.path, + cache.archive(&archive.id), ); debug!( From 3dd673677a3ddc4fcdb1b78acbecb4a581056bf0 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Wed, 10 Apr 2024 21:25:58 -0400 Subject: [PATCH 098/110] Add `--find-links` source distributions to the registry cache (#2986) ## Summary Source distributions in `--find-links` are now properly picked up in the cache. Closes https://github.com/astral-sh/uv/issues/2978. --- crates/distribution-types/src/index_url.rs | 6 ++ crates/uv-client/src/flat_index.rs | 2 +- crates/uv-client/src/registry_client.rs | 1 + .../uv-distribution/src/index/cached_wheel.rs | 15 +++- .../src/index/registry_wheel_index.rs | 88 ++++++++++++------- crates/uv-distribution/src/source/mod.rs | 88 +++++++++++-------- crates/uv/tests/pip_sync.rs | 53 ++++++++++- 7 files changed, 182 insertions(+), 71 deletions(-) diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index edfdfea3d..5de44451e 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -24,6 +24,7 @@ static DEFAULT_INDEX_URL: Lazy = pub enum IndexUrl { Pypi(VerbatimUrl), Url(VerbatimUrl), + Path(VerbatimUrl), } impl IndexUrl { @@ -32,6 +33,7 @@ impl IndexUrl { match self { Self::Pypi(url) => url.raw(), Self::Url(url) => url.raw(), + Self::Path(url) => url.raw(), } } } @@ -41,6 +43,7 @@ impl Display for IndexUrl { match self { Self::Pypi(url) => Display::fmt(url, f), Self::Url(url) => Display::fmt(url, f), + Self::Path(url) => Display::fmt(url, f), } } } @@ -50,6 +53,7 @@ impl Verbatim for IndexUrl { match self { Self::Pypi(url) => url.verbatim(), Self::Url(url) => url.verbatim(), + Self::Path(url) => url.verbatim(), } } } @@ -83,6 +87,7 @@ impl From for Url { match index { IndexUrl::Pypi(url) => url.to_url(), IndexUrl::Url(url) => url.to_url(), + IndexUrl::Path(url) => url.to_url(), } } } @@ -94,6 +99,7 @@ impl Deref for IndexUrl { match &self { Self::Pypi(url) => url, Self::Url(url) => url, + Self::Path(url) => url, } } } diff --git a/crates/uv-client/src/flat_index.rs b/crates/uv-client/src/flat_index.rs index 623e9b26e..a468cc640 100644 --- a/crates/uv-client/src/flat_index.rs +++ b/crates/uv-client/src/flat_index.rs @@ -205,7 +205,7 @@ impl<'a> FlatIndexClient<'a> { fn read_from_directory(path: &PathBuf) -> Result { // Absolute paths are required for the URL conversion. let path = fs_err::canonicalize(path)?; - let index_url = IndexUrl::Url(VerbatimUrl::from_path(&path)); + let index_url = IndexUrl::Path(VerbatimUrl::from_path(&path)); let mut dists = Vec::new(); for entry in fs_err::read_dir(path)? { diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 6fd5b5020..5873058e0 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -285,6 +285,7 @@ impl RegistryClient { Path::new(&match index { IndexUrl::Pypi(_) => "pypi".to_string(), IndexUrl::Url(url) => cache_key::digest(&cache_key::CanonicalUrl::new(url)), + IndexUrl::Path(url) => cache_key::digest(&cache_key::CanonicalUrl::new(url)), }), format!("{package_name}.rkyv"), ); diff --git a/crates/uv-distribution/src/index/cached_wheel.rs b/crates/uv-distribution/src/index/cached_wheel.rs index eea658c8f..5efbc817b 100644 --- a/crates/uv-distribution/src/index/cached_wheel.rs +++ b/crates/uv-distribution/src/index/cached_wheel.rs @@ -20,9 +20,14 @@ pub struct CachedWheel { impl CachedWheel { /// Try to parse a distribution from a cached directory name (like `typing-extensions-4.8.0-py3-none-any`). - pub fn from_built_source(path: &Path) -> Option { + pub fn from_built_source(path: impl AsRef) -> Option { + let path = path.as_ref(); + + // Determine the wheel filename. let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_stem(filename).ok()?; + + // Convert to a cached wheel. let archive = path.canonicalize().ok()?; let entry = CacheEntry::from_path(archive); let hashes = Vec::new(); @@ -54,7 +59,9 @@ impl CachedWheel { } /// Read a cached wheel from a `.http` pointer (e.g., `anyio-4.0.0-py3-none-any.http`). - pub fn from_http_pointer(path: &Path, cache: &Cache) -> Option { + pub fn from_http_pointer(path: impl AsRef, cache: &Cache) -> Option { + let path = path.as_ref(); + // Determine the wheel filename. let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_stem(filename).ok()?; @@ -73,7 +80,9 @@ impl CachedWheel { } /// Read a cached wheel from a `.rev` pointer (e.g., `anyio-4.0.0-py3-none-any.rev`). - pub fn from_local_pointer(path: &Path, cache: &Cache) -> Option { + pub fn from_local_pointer(path: impl AsRef, cache: &Cache) -> Option { + let path = path.as_ref(); + // Determine the wheel filename. let filename = path.file_name()?.to_str()?; let filename = WheelFilename::from_stem(filename).ok()?; diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 4cf9c0594..29b815d1a 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -13,7 +13,7 @@ use uv_normalize::PackageName; use uv_types::HashStrategy; use crate::index::cached_wheel::CachedWheel; -use crate::source::{HttpRevisionPointer, HTTP_REVISION}; +use crate::source::{HttpRevisionPointer, LocalRevisionPointer, HTTP_REVISION, LOCAL_REVISION}; /// A local index of distributions that originate from a registry, like `PyPI`. #[derive(Debug)] @@ -88,13 +88,13 @@ impl<'a> RegistryWheelIndex<'a> { ) -> BTreeMap { let mut versions = BTreeMap::new(); - // Collect into owned `IndexUrl` + // Collect into owned `IndexUrl`. let flat_index_urls: Vec = index_locations .flat_index() .filter_map(|flat_index| match flat_index { FlatIndexLocation::Path(path) => { let path = fs_err::canonicalize(path).ok()?; - Some(IndexUrl::Url(VerbatimUrl::from_path(path))) + Some(IndexUrl::Path(VerbatimUrl::from_path(path))) } FlatIndexLocation::Url(url) => { Some(IndexUrl::Url(VerbatimUrl::unknown(url.clone()))) @@ -112,30 +112,37 @@ impl<'a> RegistryWheelIndex<'a> { // For registry wheels, the cache structure is: `//.http` // or `///.rev`. for file in files(&wheel_dir) { - if file - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("http")) - { - if let Some(wheel) = - CachedWheel::from_http_pointer(&wheel_dir.join(&file), cache) - { - // Enforce hash-checking based on the built distribution. - if wheel.satisfies(hasher.get(package)) { - Self::add_wheel(wheel, tags, &mut versions); + match index_url { + // Add files from remote registries. + IndexUrl::Pypi(_) | IndexUrl::Url(_) => { + if file + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("http")) + { + if let Some(wheel) = + CachedWheel::from_http_pointer(wheel_dir.join(file), cache) + { + // Enforce hash-checking based on the built distribution. + if wheel.satisfies(hasher.get(package)) { + Self::add_wheel(wheel, tags, &mut versions); + } + } } } - } - - if file - .extension() - .is_some_and(|ext| ext.eq_ignore_ascii_case("rev")) - { - if let Some(wheel) = - CachedWheel::from_local_pointer(&wheel_dir.join(&file), cache) - { - // Enforce hash-checking based on the built distribution. - if wheel.satisfies(hasher.get(package)) { - Self::add_wheel(wheel, tags, &mut versions); + // Add files from local registries (e.g., `--find-links`). + IndexUrl::Path(_) => { + if file + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("rev")) + { + if let Some(wheel) = + CachedWheel::from_local_pointer(wheel_dir.join(file), cache) + { + // Enforce hash-checking based on the built distribution. + if wheel.satisfies(hasher.get(package)) { + Self::add_wheel(wheel, tags, &mut versions); + } + } } } } @@ -152,18 +159,39 @@ impl<'a> RegistryWheelIndex<'a> { for shard in directories(&cache_shard) { // Read the existing metadata from the cache, if it exists. let cache_shard = cache_shard.shard(shard); - let revision_entry = cache_shard.entry(HTTP_REVISION); - if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(&revision_entry) { + + // Read the revision from the cache. + let revision = match index_url { + // Add files from remote registries. + IndexUrl::Pypi(_) | IndexUrl::Url(_) => { + let revision_entry = cache_shard.entry(HTTP_REVISION); + if let Ok(Some(pointer)) = HttpRevisionPointer::read_from(revision_entry) { + Some(pointer.into_revision()) + } else { + None + } + } + // Add files from local registries (e.g., `--find-links`). + IndexUrl::Path(_) => { + let revision_entry = cache_shard.entry(LOCAL_REVISION); + if let Ok(Some(pointer)) = LocalRevisionPointer::read_from(revision_entry) { + Some(pointer.into_revision()) + } else { + None + } + } + }; + + if let Some(revision) = revision { // Enforce hash-checking based on the source distribution. - let revision = pointer.into_revision(); if revision.satisfies(hasher.get(package)) { for wheel_dir in symlinks(cache_shard.join(revision.id())) { - if let Some(wheel) = CachedWheel::from_built_source(&wheel_dir) { + if let Some(wheel) = CachedWheel::from_built_source(wheel_dir) { Self::add_wheel(wheel, tags, &mut versions); } } } - }; + } } } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index 78ffb00c4..474211f4c 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -87,6 +87,15 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { ) -> Result { let built_wheel_metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { + // For registry source distributions, shard by package, then version, for + // convenience in debugging. + let cache_shard = self.build_context.cache().shard( + CacheBucket::BuiltWheels, + WheelCache::Index(&dist.index) + .wheel_dir(dist.filename.name.as_ref()) + .join(dist.filename.version.to_string()), + ); + let url = match &dist.file.url { FileLocation::RelativeUrl(base, url) => { pypi_types::base_url_join_relative(base, url)? @@ -103,6 +112,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { url: &url, path: Cow::Borrowed(path), }, + &cache_shard, tags, hashes, ) @@ -111,15 +121,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } }; - // For registry source distributions, shard by package, then version, for - // convenience in debugging. - let cache_shard = self.build_context.cache().shard( - CacheBucket::BuiltWheels, - WheelCache::Index(&dist.index) - .wheel_dir(dist.filename.name.as_ref()) - .join(dist.filename.version.to_string()), - ); - self.url( source, &dist.file.filename, @@ -165,9 +166,19 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .boxed() .await? } else { - self.archive(source, &PathSourceUrl::from(dist), tags, hashes) - .boxed() - .await? + let cache_shard = self + .build_context + .cache() + .shard(CacheBucket::BuiltWheels, WheelCache::Path(&dist.url).root()); + self.archive( + source, + &PathSourceUrl::from(dist), + &cache_shard, + tags, + hashes, + ) + .boxed() + .await? } } BuildableSource::Url(SourceUrl::Direct(resource)) => { @@ -204,7 +215,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .boxed() .await? } else { - self.archive(source, resource, tags, hashes).boxed().await? + let cache_shard = self.build_context.cache().shard( + CacheBucket::BuiltWheels, + WheelCache::Path(resource.url).root(), + ); + self.archive(source, resource, &cache_shard, tags, hashes) + .boxed() + .await? } } }; @@ -222,6 +239,14 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { ) -> Result { let metadata = match &source { BuildableSource::Dist(SourceDist::Registry(dist)) => { + // For registry source distributions, shard by package, then version. + let cache_shard = self.build_context.cache().shard( + CacheBucket::BuiltWheels, + WheelCache::Index(&dist.index) + .wheel_dir(dist.filename.name.as_ref()) + .join(dist.filename.version.to_string()), + ); + let url = match &dist.file.url { FileLocation::RelativeUrl(base, url) => { pypi_types::base_url_join_relative(base, url)? @@ -238,6 +263,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { url: &url, path: Cow::Borrowed(path), }, + &cache_shard, hashes, ) .boxed() @@ -245,14 +271,6 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { } }; - // For registry source distributions, shard by package, then version. - let cache_shard = self.build_context.cache().shard( - CacheBucket::BuiltWheels, - WheelCache::Index(&dist.index) - .wheel_dir(dist.filename.name.as_ref()) - .join(dist.filename.version.to_string()), - ); - self.url_metadata( source, &dist.file.filename, @@ -296,7 +314,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .boxed() .await? } else { - self.archive_metadata(source, &PathSourceUrl::from(dist), hashes) + let cache_shard = self + .build_context + .cache() + .shard(CacheBucket::BuiltWheels, WheelCache::Path(&dist.url).root()); + self.archive_metadata(source, &PathSourceUrl::from(dist), &cache_shard, hashes) .boxed() .await? } @@ -334,7 +356,11 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .boxed() .await? } else { - self.archive_metadata(source, resource, hashes) + let cache_shard = self.build_context.cache().shard( + CacheBucket::BuiltWheels, + WheelCache::Path(resource.url).root(), + ); + self.archive_metadata(source, resource, &cache_shard, hashes) .boxed() .await? } @@ -573,17 +599,13 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, + cache_shard: &CacheShard, tags: &Tags, hashes: HashPolicy<'_>, ) -> Result { - let cache_shard = self.build_context.cache().shard( - CacheBucket::BuiltWheels, - WheelCache::Path(resource.url).root(), - ); - // Fetch the revision for the source distribution. let revision = self - .archive_revision(source, resource, &cache_shard, hashes) + .archive_revision(source, resource, cache_shard, hashes) .await?; // Before running the build, check that the hashes match. @@ -644,16 +666,12 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, source: &BuildableSource<'_>, resource: &PathSourceUrl<'_>, + cache_shard: &CacheShard, hashes: HashPolicy<'_>, ) -> Result { - let cache_shard = self.build_context.cache().shard( - CacheBucket::BuiltWheels, - WheelCache::Path(resource.url).root(), - ); - // Fetch the revision for the source distribution. let revision = self - .archive_revision(source, resource, &cache_shard, hashes) + .archive_revision(source, resource, cache_shard, hashes) .await?; // Before running the build, check that the hashes match. diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index 37365b8a2..d39c103e2 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -2539,12 +2539,12 @@ fn find_links_offline_no_match() -> Result<()> { /// Sync using `--find-links` with a local directory. Ensure that cached wheels are reused. #[test] -fn find_links_cache() -> Result<()> { +fn find_links_wheel_cache() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt.write_str(indoc! {r" - tqdm + tqdm==1000.0.0 "})?; // Install `tqdm`. @@ -2585,6 +2585,55 @@ fn find_links_cache() -> Result<()> { Ok(()) } +/// Sync using `--find-links` with a local directory. Ensure that cached source distributions are +/// reused. +#[test] +fn find_links_source_cache() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc! {r" + tqdm==999.0.0 + "})?; + + // Install `tqdm`. + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + tqdm==999.0.0 + "### + ); + + // Reinstall `tqdm` with `--reinstall`. Ensure that the wheel is reused. + uv_snapshot!(context.filters(), command(&context) + .arg("requirements.txt") + .arg("--reinstall") + .arg("--find-links") + .arg(context.workspace_root.join("scripts/links/")), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Uninstalled 1 package in [TIME] + Installed 1 package in [TIME] + - tqdm==999.0.0 + + tqdm==999.0.0 + "### + ); + + Ok(()) +} + /// Install without network access via the `--offline` flag. #[test] fn offline() -> Result<()> { From 5f59e301064854420801834d62a5473f0a3f433d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 00:55:41 -0400 Subject: [PATCH 099/110] Remove editable field from `RequirementEntry` (#2987) ## Summary It turns out this isn't used? We have a separate `EditableRequirement`. --- crates/requirements-txt/src/lib.rs | 8 -------- .../requirements_txt__test__line-endings-basic.txt.snap | 6 ------ ...rements_txt__test__line-endings-constraints-a.txt.snap | 1 - ...rements_txt__test__line-endings-constraints-b.txt.snap | 2 -- ...requirements_txt__test__line-endings-editable.txt.snap | 2 -- ...quirements_txt__test__line-endings-for-poetry.txt.snap | 4 ---- ...equirements_txt__test__line-endings-include-a.txt.snap | 2 -- ...equirements_txt__test__line-endings-include-b.txt.snap | 1 - ...ts_txt__test__line-endings-poetry-with-hashes.txt.snap | 5 ----- .../requirements_txt__test__line-endings-small.txt.snap | 2 -- ...quirements_txt__test__line-endings-whitespace.txt.snap | 2 -- .../requirements_txt__test__parse-basic.txt.snap | 6 ------ .../requirements_txt__test__parse-constraints-a.txt.snap | 1 - .../requirements_txt__test__parse-constraints-b.txt.snap | 2 -- .../requirements_txt__test__parse-for-poetry.txt.snap | 4 ---- .../requirements_txt__test__parse-include-a.txt.snap | 2 -- .../requirements_txt__test__parse-include-b.txt.snap | 1 - ...uirements_txt__test__parse-poetry-with-hashes.txt.snap | 5 ----- .../requirements_txt__test__parse-small.txt.snap | 2 -- .../requirements_txt__test__parse-unix-bare-url.txt.snap | 3 --- .../requirements_txt__test__parse-whitespace.txt.snap | 2 -- ...equirements_txt__test__parse-windows-bare-url.txt.snap | 3 --- crates/uv-requirements/src/upgrade.rs | 1 - 23 files changed, 67 deletions(-) diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 17732dc12..137f8b235 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -299,15 +299,10 @@ pub struct RequirementEntry { pub requirement: RequirementsTxtRequirement, /// Hashes of the downloadable packages pub hashes: Vec, - /// Editable installation, see e.g. - pub editable: bool, } impl Display for RequirementEntry { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - if self.editable { - write!(f, "-e ")?; - } write!(f, "{}", self.requirement)?; for hash in &self.hashes { write!(f, " --hash {hash}")?; @@ -670,7 +665,6 @@ fn parse_entry( RequirementsTxtStatement::RequirementEntry(RequirementEntry { requirement, hashes, - editable: false, }) } else if let Some(char) = s.peek() { let (line, column) = calculate_row_column(content, s.cursor()); @@ -1743,7 +1737,6 @@ mod test { }, ), hashes: [], - editable: false, }, ], constraints: [], @@ -1798,7 +1791,6 @@ mod test { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap index aeefa208a..1ea308583 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-basic.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -52,7 +51,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -77,7 +75,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -102,7 +99,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -127,7 +123,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -152,7 +147,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap index 3bae2fc6f..eed853303 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-a.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [ diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap index 8c311f4b9..2ac70d195 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-constraints-b.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -52,7 +51,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap index e8a8a5090..ede00ad21 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-editable.txt.snap @@ -16,7 +16,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -57,7 +56,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap index 513d09259..ff6db6eab 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-for-poetry.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -52,7 +51,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -66,7 +64,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -99,7 +96,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap index f14751af9..82edeab41 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-a.txt.snap @@ -16,7 +16,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -41,7 +40,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap index 07f69a01f..745bacabc 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-include-b.txt.snap @@ -16,7 +16,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap index 318f3047d..25e816bb5 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-poetry-with-hashes.txt.snap @@ -56,7 +56,6 @@ RequirementsTxt { hashes: [ "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", ], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -110,7 +109,6 @@ RequirementsTxt { hashes: [ "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", ], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -175,7 +173,6 @@ RequirementsTxt { hashes: [ "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", ], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -230,7 +227,6 @@ RequirementsTxt { "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a", ], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -287,7 +283,6 @@ RequirementsTxt { "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e", "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a", ], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap index 717c063d4..bc4cb551f 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-small.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -52,7 +51,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap index e8a8a5090..ede00ad21 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__line-endings-whitespace.txt.snap @@ -16,7 +16,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -57,7 +56,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap index aeefa208a..1ea308583 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-basic.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -52,7 +51,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -77,7 +75,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -102,7 +99,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -127,7 +123,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -152,7 +147,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap index 3bae2fc6f..eed853303 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-a.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [ diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap index 8c311f4b9..2ac70d195 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-constraints-b.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -52,7 +51,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap index 513d09259..ff6db6eab 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-for-poetry.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -52,7 +51,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -66,7 +64,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -99,7 +96,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap index f14751af9..82edeab41 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-a.txt.snap @@ -16,7 +16,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -41,7 +40,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap index 07f69a01f..745bacabc 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-include-b.txt.snap @@ -16,7 +16,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap index 318f3047d..25e816bb5 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-poetry-with-hashes.txt.snap @@ -56,7 +56,6 @@ RequirementsTxt { hashes: [ "sha256:2e1ccc9417d4da358b9de6f174e3ac094391ea1d4fbef2d667865d819dfd0afe", ], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -110,7 +109,6 @@ RequirementsTxt { hashes: [ "sha256:8a388717b9476f934a21484e8c8e61875ab60644d29b9b39e11e4b9dc1c6b305", ], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -175,7 +173,6 @@ RequirementsTxt { hashes: [ "sha256:e4d039def5768a47e4afec8e89e83ec3ae5a26bf00ad851f914d1240b444d2b1", ], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -230,7 +227,6 @@ RequirementsTxt { "sha256:2577c501a2fb8d05a304c09d090d6e47c306fef15809d102b327cf8364bddab5", "sha256:75beac4a47881eeb94d5ea5d6ad31ef88856affe2332b9aafb52c6452ccf0d7a", ], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -287,7 +283,6 @@ RequirementsTxt { "sha256:1a5c7d7d577e0eabfcf15eb87d1e19314c8c4f0e722a301f98e0e3a65e238b4e", "sha256:1e5a38aa85bd660c53947bd28aeaafb6a97d70423606f1ccb044a03a1203fe4a", ], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap index 717c063d4..bc4cb551f 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-small.txt.snap @@ -27,7 +27,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -52,7 +51,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap index 1e71a4be4..6ebf240d1 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-unix-bare-url.txt.snap @@ -28,7 +28,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Unnamed( @@ -58,7 +57,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Unnamed( @@ -84,7 +82,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap index e8a8a5090..ede00ad21 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-whitespace.txt.snap @@ -16,7 +16,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Pep508( @@ -57,7 +56,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap index d78405047..0f05ef8ea 100644 --- a/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap +++ b/crates/requirements-txt/src/snapshots/requirements_txt__test__parse-windows-bare-url.txt.snap @@ -28,7 +28,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Unnamed( @@ -58,7 +57,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, RequirementEntry { requirement: Unnamed( @@ -84,7 +82,6 @@ RequirementsTxt { }, ), hashes: [], - editable: false, }, ], constraints: [], diff --git a/crates/uv-requirements/src/upgrade.rs b/crates/uv-requirements/src/upgrade.rs index 46c22ca0e..8dc92c5c2 100644 --- a/crates/uv-requirements/src/upgrade.rs +++ b/crates/uv-requirements/src/upgrade.rs @@ -30,7 +30,6 @@ pub async fn read_lockfile( let preferences = requirements_txt .requirements .into_iter() - .filter(|entry| !entry.editable) .map(Preference::from_entry) .collect::, PreferenceError>>()?; From c85c52d4cecc7e63f2cc29190e45f3e1193f6a2d Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 11 Apr 2024 10:35:22 +0200 Subject: [PATCH 100/110] Unify packse find links urls (#2969) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sync scenarios script is broken, so i did the updates manually ``` $ ./scripts/sync_scenarios.sh Setting up a temporary environment... Using Python 3.12.1 interpreter at: /home/konsti/projects/uv/.venv/bin/python3 Creating virtualenv at: .venv Activate with: source .venv/bin/activate × No solution found when resolving dependencies: ╰─▶ Because docutils==0.21.post1 is unusable because the package metadata was inconsistent and you require docutils==0.21.post1, we can conclude that the requirements are unsatisfiable. hint: Metadata for docutils==0.21.post1 was inconsistent: Package metadata version `0.21` does not match given version `0.21.post1` ``` --------- Co-authored-by: Zanie Blue --- crates/uv-git/src/source.rs | 2 +- crates/uv/tests/common/mod.rs | 5 +++++ crates/uv/tests/pip_compile_scenarios.rs | 6 +++--- crates/uv/tests/pip_install.rs | 18 +++++++++--------- crates/uv/tests/pip_install_scenarios.rs | 6 +++--- scripts/scenarios/generate.py | 5 ++--- scripts/scenarios/requirements.txt | 2 +- 7 files changed, 24 insertions(+), 20 deletions(-) diff --git a/crates/uv-git/src/source.rs b/crates/uv-git/src/source.rs index f1061331a..64483d038 100644 --- a/crates/uv-git/src/source.rs +++ b/crates/uv-git/src/source.rs @@ -49,7 +49,7 @@ impl GitSource { } /// Fetch the underlying Git repository at the given revision. - #[instrument(skip(self))] + #[instrument(skip(self), fields(repository = %self.git.repository, rev = ?self.git.precise))] pub fn fetch(self) -> Result { // The path to the repo, within the Git database. let ident = digest(&RepositoryUrl::new(&self.git.repository)); diff --git a/crates/uv/tests/common/mod.rs b/crates/uv/tests/common/mod.rs index 2b77cc9c5..2c85e3e7f 100644 --- a/crates/uv/tests/common/mod.rs +++ b/crates/uv/tests/common/mod.rs @@ -22,6 +22,11 @@ use uv_toolchain::{toolchains_for_version, PythonVersion}; // Exclude any packages uploaded after this date. pub static EXCLUDE_NEWER: &str = "2024-03-25T00:00:00Z"; +/// Using a find links url allows using `--index-url` instead of `--extra-index-url` in tests +/// to prevent dependency confusion attacks against our test suite. +pub const BUILD_VENDOR_LINKS_URL: &str = + "https://raw.githubusercontent.com/astral-sh/packse/0.3.14/vendor/links.html"; + #[doc(hidden)] // Macro and test context only, don't use directly. pub const INSTA_FILTERS: &[(&str, &str)] = &[ (r"--cache-dir [^\s]+", "--cache-dir [CACHE_DIR]"), diff --git a/crates/uv/tests/pip_compile_scenarios.rs b/crates/uv/tests/pip_compile_scenarios.rs index 780c5fd6d..c5a920a12 100644 --- a/crates/uv/tests/pip_compile_scenarios.rs +++ b/crates/uv/tests/pip_compile_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] @@ -27,9 +27,9 @@ fn command(context: &TestContext, python_versions: &[&str]) -> Command { .arg("compile") .arg("requirements.in") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.13/simple-html/") + .arg("https://astral-sh.github.io/packse/0.3.14/simple-html/") .arg("--find-links") - .arg("https://raw.githubusercontent.com/zanieb/packse/0.3.13/vendor/links.html") + .arg("https://raw.githubusercontent.com/astral-sh/packse/0.3.14/vendor/links.html") .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 055d84f36..0832912a3 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -1,5 +1,7 @@ #![cfg(all(feature = "python", feature = "pypi"))] +use std::process::Command; + use anyhow::Result; use assert_cmd::prelude::*; use assert_fs::prelude::*; @@ -7,12 +9,10 @@ use base64::{prelude::BASE64_STANDARD as base64, Engine}; use indoc::indoc; use itertools::Itertools; -use std::process::Command; - use common::{uv_snapshot, TestContext}; use uv_fs::Simplified; -use crate::common::get_bin; +use crate::common::{get_bin, BUILD_VENDOR_LINKS_URL}; mod common; @@ -3150,7 +3150,7 @@ fn already_installed_dependent_editable() { // Disable the index to guard this test against dependency confusion attacks .arg("--no-index") .arg("--find-links") - .arg("https://raw.githubusercontent.com/astral-sh/packse/0.3.12/vendor/links.html"), @r###" + .arg(BUILD_VENDOR_LINKS_URL), @r###" success: true exit_code: 0 ----- stdout ----- @@ -3185,7 +3185,7 @@ fn already_installed_dependent_editable() { // Disable the index to guard this test against dependency confusion attacks .arg("--no-index") .arg("--find-links") - .arg("https://raw.githubusercontent.com/astral-sh/packse/0.3.12/vendor/links.html"), @r###" + .arg(BUILD_VENDOR_LINKS_URL), @r###" success: false exit_code: 1 ----- stdout ----- @@ -3246,7 +3246,7 @@ fn already_installed_local_path_dependent() { // Disable the index to guard this test against dependency confusion attacks .arg("--no-index") .arg("--find-links") - .arg("https://raw.githubusercontent.com/astral-sh/packse/0.3.12/vendor/links.html"), @r###" + .arg(BUILD_VENDOR_LINKS_URL), @r###" success: true exit_code: 0 ----- stdout ----- @@ -3281,7 +3281,7 @@ fn already_installed_local_path_dependent() { // Disable the index to guard this test against dependency confusion attacks .arg("--no-index") .arg("--find-links") - .arg("https://raw.githubusercontent.com/astral-sh/packse/0.3.12/vendor/links.html"), @r###" + .arg(BUILD_VENDOR_LINKS_URL), @r###" success: false exit_code: 1 ----- stdout ----- @@ -3321,7 +3321,7 @@ fn already_installed_local_path_dependent() { // Disable the index to guard this test against dependency confusion attacks .arg("--no-index") .arg("--find-links") - .arg("https://raw.githubusercontent.com/astral-sh/packse/0.3.12/vendor/links.html"), @r###" + .arg(BUILD_VENDOR_LINKS_URL), @r###" success: false exit_code: 1 ----- stdout ----- @@ -3343,7 +3343,7 @@ fn already_installed_local_path_dependent() { // Disable the index to guard this test against dependency confusion attacks .arg("--no-index") .arg("--find-links") - .arg("https://raw.githubusercontent.com/astral-sh/packse/0.3.12/vendor/links.html"), @r###" + .arg(BUILD_VENDOR_LINKS_URL), @r###" success: true exit_code: 0 ----- stdout ----- diff --git a/crates/uv/tests/pip_install_scenarios.rs b/crates/uv/tests/pip_install_scenarios.rs index 001cba3c1..43c399e1b 100644 --- a/crates/uv/tests/pip_install_scenarios.rs +++ b/crates/uv/tests/pip_install_scenarios.rs @@ -1,7 +1,7 @@ //! DO NOT EDIT //! //! Generated with `./scripts/sync_scenarios.sh` -//! Scenarios from +//! Scenarios from //! #![cfg(all(feature = "python", feature = "pypi", unix))] @@ -46,9 +46,9 @@ fn command(context: &TestContext) -> Command { .arg("pip") .arg("install") .arg("--index-url") - .arg("https://astral-sh.github.io/packse/0.3.13/simple-html/") + .arg("https://astral-sh.github.io/packse/0.3.14/simple-html/") .arg("--find-links") - .arg("https://raw.githubusercontent.com/zanieb/packse/0.3.13/vendor/links.html") + .arg("https://raw.githubusercontent.com/astral-sh/packse/0.3.14/vendor/links.html") .arg("--cache-dir") .arg(context.cache_dir.path()) .env("VIRTUAL_ENV", context.venv.as_os_str()) diff --git a/scripts/scenarios/generate.py b/scripts/scenarios/generate.py index 207ba3379..f5a60998e 100755 --- a/scripts/scenarios/generate.py +++ b/scripts/scenarios/generate.py @@ -65,7 +65,6 @@ except ImportError: ) exit(1) - try: import chevron_blue except ImportError: @@ -175,11 +174,11 @@ def main(scenarios: list[Path], snapshot_update: bool = True): # Add generated metadata data["generated_from"] = ( - f"https://github.com/zanieb/packse/tree/{ref}/scenarios" + f"https://github.com/astral-sh/packse/tree/{ref}/scenarios" ) data["generated_with"] = "./scripts/sync_scenarios.sh" data["vendor_links"] = ( - f"https://raw.githubusercontent.com/zanieb/packse/{ref}/vendor/links.html" + f"https://raw.githubusercontent.com/astral-sh/packse/{ref}/vendor/links.html" ) data["index_url"] = f"https://astral-sh.github.io/packse/{ref}/simple-html/" diff --git a/scripts/scenarios/requirements.txt b/scripts/scenarios/requirements.txt index 7a4cc2244..1d7df3023 100644 --- a/scripts/scenarios/requirements.txt +++ b/scripts/scenarios/requirements.txt @@ -36,7 +36,7 @@ nh3==0.2.17 # via readme-renderer packaging==24.0 # via hatchling -packse==0.3.13 +packse==0.3.14 pathspec==0.12.1 # via hatchling pkginfo==1.10.0 From 7c7f06f62b80a4189a8078af5c7d571d108b3ec5 Mon Sep 17 00:00:00 2001 From: samypr100 <3933065+samypr100@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:41:09 +0000 Subject: [PATCH 101/110] feat: convert linehaul tests to use snapshots (#2923) ## Summary Closes #2564 ## Test Plan 1. Changed existing linehaul tests to leverage insta. 2. Ran tests in various linux distros (Debian, Ubuntu, Centos, Fedora, Alpine) to ensure they also pass locally again. --------- Co-authored-by: konstin --- Cargo.lock | 53 +++++++++ crates/uv-client/Cargo.toml | 2 +- crates/uv-client/tests/user_agent_version.rs | 118 ++++++++++++------- 3 files changed, 128 insertions(+), 45 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a70416778..79b79ad7f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1745,6 +1745,8 @@ dependencies = [ "console", "lazy_static", "linked-hash-map", + "pest", + "pest_derive", "regex", "serde", "similar", @@ -2420,6 +2422,51 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pest" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56f8023d0fb78c8e03784ea1c7f3fa36e68a723138990b8d5a47d916b651e7a8" +dependencies = [ + "memchr", + "thiserror", + "ucd-trie", +] + +[[package]] +name = "pest_derive" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0d24f72393fd16ab6ac5738bc33cdb6a9aa73f8b902e8fe29cf4e67d7dd1026" +dependencies = [ + "pest", + "pest_generator", +] + +[[package]] +name = "pest_generator" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc17e2a6c7d0a492f0158d7a4bd66cc17280308bbaff78d5bef566dca35ab80" +dependencies = [ + "pest", + "pest_meta", + "proc-macro2", + "quote", + "syn 2.0.58", +] + +[[package]] +name = "pest_meta" +version = "2.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "934cd7631c050f4674352a6e835d5f6711ffbfb9345c2fc0107155ac495ae293" +dependencies = [ + "once_cell", + "pest", + "sha2", +] + [[package]] name = "petgraph" version = "0.6.4" @@ -4071,6 +4118,12 @@ version = "1.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" +[[package]] +name = "ucd-trie" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed646292ffc8188ef8ea4d1e0e0150fb15a5c2e12ad9b8fc191ae7a8a7f3c4b9" + [[package]] name = "unicode-bidi" version = "0.3.15" diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 25ec0d8d1..bd8b7521d 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -51,5 +51,5 @@ anyhow = { workspace = true } http-body-util = { version = "0.1.0" } hyper = { version = "1.2.0", features = ["server", "http1"] } hyper-util = { version = "0.1.3", features = ["tokio"] } -insta = { version = "1.36.1" } +insta = { version = "1.36.1" , features = ["filters", "json", "redactions"] } tokio = { workspace = true, features = ["fs", "macros"] } diff --git a/crates/uv-client/tests/user_agent_version.rs b/crates/uv-client/tests/user_agent_version.rs index 383e0b758..2dd67f5f9 100644 --- a/crates/uv-client/tests/user_agent_version.rs +++ b/crates/uv-client/tests/user_agent_version.rs @@ -7,10 +7,10 @@ use hyper::server::conn::http1; use hyper::service::service_fn; use hyper::{Request, Response}; use hyper_util::rt::TokioIo; -use tokio::net::TcpListener; - +use insta::{assert_json_snapshot, assert_snapshot, with_settings}; use pep508_rs::{MarkerEnvironment, StringVersion}; use platform_tags::{Arch, Os, Platform}; +use tokio::net::TcpListener; use uv_cache::Cache; use uv_client::LineHaul; use uv_client::RegistryClientBuilder; @@ -179,52 +179,82 @@ async fn test_user_agent_has_linehaul() -> Result<()> { // Deserializing Linehaul let linehaul: LineHaul = serde_json::from_str(uv_linehaul)?; - // Assert uv version - assert_eq!(uv_version, format!("uv/{}", version())); - - // Assert linehaul - let installer_info = linehaul.installer.unwrap(); - let system_info = linehaul.system.unwrap(); - let impl_info = linehaul.implementation.unwrap(); - - assert_eq!(installer_info.name.as_deref(), Some("uv")); - assert_eq!(installer_info.version.as_deref(), Some(version())); - - assert_eq!(system_info.name, Some(markers.platform_system)); - assert_eq!(system_info.release, Some(markers.platform_release)); - - assert_eq!(impl_info.name, Some(markers.platform_python_implementation)); - assert_eq!( - impl_info.version, - Some(markers.python_full_version.version.to_string()) - ); - - assert_eq!( - linehaul.python, - Some(markers.python_full_version.version.to_string()) - ); - assert_eq!(linehaul.cpu, Some(markers.platform_machine)); - - assert_eq!(linehaul.openssl_version, None); - assert_eq!(linehaul.setuptools_version, None); - assert_eq!(linehaul.rustc_version, None); + // Assert linehaul user agent + let filters = vec![(version(), "[VERSION]")]; + with_settings!({ + filters => filters + }, { + // Assert uv version + assert_snapshot!(uv_version, @"uv/[VERSION]"); + // Assert linehaul json + assert_json_snapshot!(&linehaul, { + ".distro" => "[distro]", + ".ci" => "[ci]" + }, @r###" + { + "installer": { + "name": "uv", + "version": "[VERSION]" + }, + "python": "3.12.2", + "implementation": { + "name": "CPython", + "version": "3.12.2" + }, + "distro": "[distro]", + "system": { + "name": "Linux", + "release": "6.5.0-1016-azure" + }, + "cpu": "x86_64", + "openssl_version": null, + "setuptools_version": null, + "rustc_version": null, + "ci": "[ci]" + } + "###); + }); + // Assert distro if cfg!(windows) { - assert_eq!(linehaul.distro, None); + assert_json_snapshot!(&linehaul.distro, @"null"); } else if cfg!(target_os = "linux") { - let Some(distro_info) = linehaul.distro else { - panic!("got no distro, but expected one in linehaul") - }; - assert_eq!(distro_info.id.as_deref(), Some("jammy")); - assert_eq!(distro_info.name.as_deref(), Some("Ubuntu")); - assert_eq!(distro_info.version.as_deref(), Some("22.04")); - assert!(distro_info.libc.is_some()); + assert_json_snapshot!(&linehaul.distro, { + ".id" => "[distro.id]", + ".name" => "[distro.name]", + ".version" => "[distro.version]" + // We mock the libc version already + }, @r###" + { + "name": "[distro.name]", + "version": "[distro.version]", + "id": "[distro.id]", + "libc": { + "lib": "glibc", + "version": "2.38" + } + }"### + ); + // Check dynamic values + let distro_info = linehaul + .distro + .expect("got no distro, but expected one in linehaul"); + // Gather distribution info from /etc/os-release. + let release_info = sys_info::linux_os_release() + .expect("got no os release info, but expected one in linux"); + assert_eq!(distro_info.id, release_info.version_codename); + assert_eq!(distro_info.name, release_info.name); + assert_eq!(distro_info.version, release_info.version_id); } else if cfg!(target_os = "macos") { - let distro_info = linehaul.distro.unwrap(); - assert_eq!(distro_info.id, None); - assert_eq!(distro_info.name.as_deref(), Some("macOS")); - assert_eq!(distro_info.version.as_deref(), Some("14.4")); - assert_eq!(distro_info.libc, None); + // We mock the macOS distro + assert_json_snapshot!(&linehaul.distro, @r###" + { + "name": "macOS", + "version": "14.4", + "id": null, + "libc": null + }"### + ); } Ok(()) From 6e06760591be1c5988ea50aebf8f048e9ce29ed3 Mon Sep 17 00:00:00 2001 From: konsti Date: Thu, 11 Apr 2024 11:45:50 +0200 Subject: [PATCH 102/110] Silence lint false positive (#2989) When running the `uv-client` tests, i would previously get: ``` warning: field `0` is never read --> crates/uv-configuration/src/config_settings.rs:43:27 | 43 | pub struct ConfigSettings(BTreeMap); | -------------- ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ | | | field in this struct | = note: `ConfigSettings` has derived impls for the traits `Clone` and `Debug`, but these are intentionally ignored during dead code analysis = note: `#[warn(dead_code)]` on by default help: consider changing the field to be of unit type to suppress this warning while preserving the field numbering, or remove the field | 43 | pub struct ConfigSettings(()); | ~~ warning: `uv-configuration` (lib) generated 1 warning ``` --- crates/uv-configuration/src/config_settings.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/uv-configuration/src/config_settings.rs b/crates/uv-configuration/src/config_settings.rs index 7b0729aeb..c7cefc64a 100644 --- a/crates/uv-configuration/src/config_settings.rs +++ b/crates/uv-configuration/src/config_settings.rs @@ -40,6 +40,7 @@ enum ConfigSettingValue { /// /// See: #[derive(Debug, Default, Clone)] +#[cfg_attr(not(feature = "serde"), allow(dead_code))] pub struct ConfigSettings(BTreeMap); impl FromIterator for ConfigSettings { From d56d142520ce99f0c4f479cb00e152bab6be7065 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 10:16:05 -0400 Subject: [PATCH 103/110] Add a few more `--generate-hashes` cases to compile tests (#2992) --- crates/uv/tests/pip_compile.rs | 76 ++++++++++++++++++++++++++++++++++ 1 file changed, 76 insertions(+) diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index 87fcda443..f196db2a4 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -3550,6 +3550,39 @@ fn generate_hashes_built_distribution_url() -> Result<()> { Ok(()) } +/// Given a VCS dependency, include hashes for its dependencies, but not the repository itself. +#[test] +fn generate_hashes_git() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio @ git+https://github.com/agronholm/anyio@4.3.0")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--generate-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --generate-hashes + anyio @ git+https://github.com/agronholm/anyio@437a7e310925a962cab4a58fcd2455fbcd578d51 + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + + ----- stderr ----- + Resolved 3 packages in [TIME] + "### + ); + + Ok(()) +} + /// Given an unnamed URL, include hashes for the URL and its dependencies. #[test] fn generate_hashes_unnamed_url() -> Result<()> { @@ -3626,6 +3659,49 @@ fn generate_hashes_local_directory() -> Result<()> { Ok(()) } +/// Given an editable dependency, include hashes for its dependencies, but not the directory itself. +#[test] +fn generate_hashes_editable() -> Result<()> { + let _context = TestContext::new("3.12"); + + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str(indoc! {r" + -e ../../scripts/packages/poetry_editable + " + })?; + + uv_snapshot!(context.filters(), context.compile() + .arg(requirements_in.path()) + .arg("--generate-hashes") + .current_dir(current_dir()?), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z [TEMP_DIR]/requirements.in --generate-hashes + -e ../../scripts/packages/poetry_editable + anyio==4.3.0 \ + --hash=sha256:048e05d0f6caeed70d731f3db756d35dcc1f35747c8c403364a8332c630441b8 \ + --hash=sha256:f75253795a87df48568485fd18cdd2a3fa5c4f7c5be8e5e36637733fce06fed6 + # via poetry-editable + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + + ----- stderr ----- + Built 1 editable in [TIME] + Resolved 4 packages in [TIME] + "###); + + Ok(()) +} + /// Compile using `--find-links` with a local directory. #[test] fn find_links_directory() -> Result<()> { From 96c3c2e77415a7e2afc9d2f15e261db1d41acabb Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 11:26:50 -0400 Subject: [PATCH 104/110] Support unnamed requirements in `--require-hashes` (#2993) ## Summary This PR enables `--require-hashes` with unnamed requirements. The key change is that `PackageId` becomes `VersionId` (since it refers to a package at a specific version), and the new `PackageId` consists of _either_ a package name _or_ a URL. The hashes are keyed by `PackageId`, so we can generate the `RequiredHashes` before we have names for all packages, and enforce them throughout. Closes #2979. --- Cargo.lock | 1 + crates/distribution-types/src/id.rs | 46 ++++++- crates/distribution-types/src/traits.rs | 24 +++- crates/uv-build/src/lib.rs | 44 +++--- crates/uv-dispatch/src/lib.rs | 6 +- .../src/index/built_wheel_index.rs | 6 +- .../src/index/registry_wheel_index.rs | 6 +- crates/uv-installer/src/downloader.rs | 4 +- crates/uv-installer/src/plan.rs | 5 +- crates/uv-requirements/src/lookahead.rs | 6 +- crates/uv-requirements/src/source_tree.rs | 7 +- crates/uv-requirements/src/unnamed.rs | 20 +-- crates/uv-resolver/src/flat_index.rs | 4 +- crates/uv-resolver/src/resolution.rs | 28 ++-- .../src/resolver/batch_prefetch.rs | 2 +- crates/uv-resolver/src/resolver/index.rs | 12 +- crates/uv-resolver/src/resolver/mod.rs | 52 ++++--- crates/uv-resolver/src/resolver/provider.rs | 4 +- crates/uv-resolver/src/version_map.rs | 2 +- crates/uv-types/Cargo.toml | 1 + crates/uv-types/src/hash.rs | 128 ++++++++++++------ crates/uv-types/src/traits.rs | 4 +- crates/uv/src/commands/pip_install.rs | 7 +- crates/uv/src/commands/pip_sync.rs | 7 +- crates/uv/tests/pip_sync.rs | 15 +- 25 files changed, 256 insertions(+), 185 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 79b79ad7f..6a58eb025 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4842,6 +4842,7 @@ dependencies = [ "serde", "serde_json", "thiserror", + "url", "uv-cache", "uv-configuration", "uv-interpreter", diff --git a/crates/distribution-types/src/id.rs b/crates/distribution-types/src/id.rs index 9920ae2d5..cca3237da 100644 --- a/crates/distribution-types/src/id.rs +++ b/crates/distribution-types/src/id.rs @@ -1,32 +1,64 @@ use std::fmt::{Display, Formatter}; +use cache_key::CanonicalUrl; use url::Url; use pep440_rs::Version; use uv_normalize::PackageName; -/// A unique identifier for a package at a specific version (e.g., `black==23.10.0`). +/// A unique identifier for a package. A package can either be identified by a name (e.g., `black`) +/// or a URL (e.g., `git+https://github.com/psf/black`). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] pub enum PackageId { - /// The identifier consists of a package name and version. - NameVersion(PackageName, Version), + /// The identifier consists of a package name. + Name(PackageName), /// The identifier consists of a URL. Url(String), } impl PackageId { /// Create a new [`PackageId`] from a package name and version. - pub fn from_registry(name: PackageName, version: Version) -> Self { - Self::NameVersion(name, version) + pub fn from_registry(name: PackageName) -> Self { + Self::Name(name) } /// Create a new [`PackageId`] from a URL. pub fn from_url(url: &Url) -> Self { - Self::Url(cache_key::digest(&cache_key::CanonicalUrl::new(url))) + Self::Url(cache_key::digest(&CanonicalUrl::new(url))) } } impl Display for PackageId { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + match self { + Self::Name(name) => write!(f, "{name}"), + Self::Url(url) => write!(f, "{url}"), + } + } +} + +/// A unique identifier for a package at a specific version (e.g., `black==23.10.0`). +#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub enum VersionId { + /// The identifier consists of a package name and version. + NameVersion(PackageName, Version), + /// The identifier consists of a URL. + Url(String), +} + +impl VersionId { + /// Create a new [`VersionId`] from a package name and version. + pub fn from_registry(name: PackageName, version: Version) -> Self { + Self::NameVersion(name, version) + } + + /// Create a new [`VersionId`] from a URL. + pub fn from_url(url: &Url) -> Self { + Self::Url(cache_key::digest(&CanonicalUrl::new(url))) + } +} + +impl Display for VersionId { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { Self::NameVersion(name, version) => write!(f, "{name}-{version}"), @@ -73,7 +105,7 @@ impl ResourceId { } } -impl From<&Self> for PackageId { +impl From<&Self> for VersionId { /// Required for `WaitMap::wait`. fn from(value: &Self) -> Self { value.clone() diff --git a/crates/distribution-types/src/traits.rs b/crates/distribution-types/src/traits.rs index c5beacbf5..5d87219dc 100644 --- a/crates/distribution-types/src/traits.rs +++ b/crates/distribution-types/src/traits.rs @@ -10,7 +10,8 @@ use crate::{ BuiltDist, CachedDirectUrlDist, CachedDist, CachedRegistryDist, DirectUrlBuiltDist, DirectUrlSourceDist, Dist, DistributionId, GitSourceDist, InstalledDirectUrlDist, InstalledDist, InstalledRegistryDist, InstalledVersion, LocalDist, PackageId, PathBuiltDist, - PathSourceDist, RegistryBuiltDist, RegistrySourceDist, ResourceId, SourceDist, VersionOrUrl, + PathSourceDist, RegistryBuiltDist, RegistrySourceDist, ResourceId, SourceDist, VersionId, + VersionOrUrl, }; pub trait Name { @@ -25,16 +26,29 @@ pub trait DistributionMetadata: Name { /// for URL-based distributions. fn version_or_url(&self) -> VersionOrUrl; - /// Returns a unique identifier for the package (e.g., `black==23.10.0`). + /// Returns a unique identifier for the package at the given version (e.g., `black==23.10.0`). /// /// Note that this is not equivalent to a unique identifier for the _distribution_, as multiple /// registry-based distributions (e.g., different wheels for the same package and version) - /// will return the same package ID, but different distribution IDs. - fn package_id(&self) -> PackageId { + /// will return the same version ID, but different distribution IDs. + fn version_id(&self) -> VersionId { match self.version_or_url() { VersionOrUrl::Version(version) => { - PackageId::from_registry(self.name().clone(), version.clone()) + VersionId::from_registry(self.name().clone(), version.clone()) } + VersionOrUrl::Url(url) => VersionId::from_url(url), + } + } + + /// Returns a unique identifier for a package. A package can either be identified by a name + /// (e.g., `black`) or a URL (e.g., `git+https://github.com/psf/black`). + /// + /// Note that this is not equivalent to a unique identifier for the _distribution_, as multiple + /// registry-based distributions (e.g., different wheels for the same package and version) + /// will return the same version ID, but different distribution IDs. + fn package_id(&self) -> PackageId { + match self.version_or_url() { + VersionOrUrl::Version(_) => PackageId::from_registry(self.name().clone()), VersionOrUrl::Url(url) => PackageId::from_url(url), } } diff --git a/crates/uv-build/src/lib.rs b/crates/uv-build/src/lib.rs index 5933511aa..7d8ff3795 100644 --- a/crates/uv-build/src/lib.rs +++ b/crates/uv-build/src/lib.rs @@ -112,7 +112,7 @@ pub enum MissingLibrary { #[derive(Debug, Error)] pub struct MissingHeaderCause { missing_library: MissingLibrary, - package_id: String, + version_id: String, } impl Display for MissingHeaderCause { @@ -122,22 +122,22 @@ impl Display for MissingHeaderCause { write!( f, "This error likely indicates that you need to install a library that provides \"{}\" for {}", - header, self.package_id + header, self.version_id ) } MissingLibrary::Linker(library) => { write!( f, "This error likely indicates that you need to install the library that provides a shared library \ - for {library} for {package_id} (e.g. lib{library}-dev)", - library = library, package_id = self.package_id + for {library} for {version_id} (e.g. lib{library}-dev)", + library = library, version_id = self.version_id ) } MissingLibrary::PythonPackage(package) => { write!( f, - "This error likely indicates that you need to `uv pip install {package}` into the build environment for {package_id}", - package = package, package_id = self.package_id + "This error likely indicates that you need to `uv pip install {package}` into the build environment for {version_id}", + package = package, version_id = self.version_id ) } } @@ -148,7 +148,7 @@ impl Error { fn from_command_output( message: String, output: &Output, - package_id: impl Into, + version_id: impl Into, ) -> Self { let stdout = String::from_utf8_lossy(&output.stdout).trim().to_string(); let stderr = String::from_utf8_lossy(&output.stderr).trim().to_string(); @@ -178,7 +178,7 @@ impl Error { stderr, missing_header_cause: MissingHeaderCause { missing_library, - package_id: package_id.into(), + version_id: version_id.into(), }, }; } @@ -364,7 +364,7 @@ pub struct SourceBuild { /// > it created. metadata_directory: Option, /// Package id such as `foo-1.2.3`, for error reporting - package_id: String, + version_id: String, /// Whether we do a regular PEP 517 build or an PEP 660 editable build build_kind: BuildKind, /// Modified PATH that contains the `venv_bin`, `user_path` and `system_path` variables in that order @@ -385,7 +385,7 @@ impl SourceBuild { interpreter: &Interpreter, build_context: &impl BuildContext, source_build_context: SourceBuildContext, - package_id: String, + version_id: String, setup_py: SetupPyStrategy, config_settings: ConfigSettings, build_isolation: BuildIsolation<'_>, @@ -477,7 +477,7 @@ impl SourceBuild { &venv, pep517_backend, build_context, - &package_id, + &version_id, build_kind, &config_settings, &environment_variables, @@ -497,7 +497,7 @@ impl SourceBuild { build_kind, config_settings, metadata_directory: None, - package_id, + version_id, environment_variables, modified_path, }) @@ -695,7 +695,7 @@ impl SourceBuild { return Err(Error::from_command_output( "Build backend failed to determine metadata through `prepare_metadata_for_build_wheel`".to_string(), &output, - &self.package_id, + &self.version_id, )); } @@ -714,7 +714,7 @@ impl SourceBuild { /// dir. /// /// - #[instrument(skip_all, fields(package_id = self.package_id))] + #[instrument(skip_all, fields(version_id = self.version_id))] pub async fn build_wheel(&self, wheel_dir: &Path) -> Result { // The build scripts run with the extracted root as cwd, so they need the absolute path. let wheel_dir = fs::canonicalize(wheel_dir)?; @@ -750,7 +750,7 @@ impl SourceBuild { return Err(Error::from_command_output( "Failed building wheel through setup.py".to_string(), &output, - &self.package_id, + &self.version_id, )); } let dist = fs::read_dir(self.source_tree.join("dist"))?; @@ -761,7 +761,7 @@ impl SourceBuild { "Expected exactly wheel in `dist/` after invoking setup.py, found {dist_dir:?}" ), &output, - &self.package_id) + &self.version_id) ); }; @@ -831,7 +831,7 @@ impl SourceBuild { self.build_kind ), &output, - &self.package_id, + &self.version_id, )); } @@ -843,7 +843,7 @@ impl SourceBuild { self.build_kind ), &output, - &self.package_id, + &self.version_id, )); } Ok(distribution_filename) @@ -873,7 +873,7 @@ async fn create_pep517_build_environment( venv: &PythonEnvironment, pep517_backend: &Pep517Backend, build_context: &impl BuildContext, - package_id: &str, + version_id: &str, build_kind: BuildKind, config_settings: &ConfigSettings, environment_variables: &FxHashMap, @@ -927,7 +927,7 @@ async fn create_pep517_build_environment( return Err(Error::from_command_output( format!("Build backend failed to determine extra requires with `build_{build_kind}()`"), &output, - package_id, + version_id, )); } @@ -938,7 +938,7 @@ async fn create_pep517_build_environment( "Build backend failed to read extra requires from `get_requires_for_build_{build_kind}`: {err}" ), &output, - package_id, + version_id, ) })?; @@ -949,7 +949,7 @@ async fn create_pep517_build_environment( "Build backend failed to return extra requires with `get_requires_for_build_{build_kind}`: {err}" ), &output, - package_id, + version_id, ) })?; diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index dab5b2cbe..b204b9a1f 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -269,12 +269,12 @@ impl<'a> BuildContext for BuildDispatch<'a> { Ok(()) } - #[instrument(skip_all, fields(package_id = package_id, subdirectory = ?subdirectory))] + #[instrument(skip_all, fields(version_id = version_id, subdirectory = ?subdirectory))] async fn setup_build<'data>( &'data self, source: &'data Path, subdirectory: Option<&'data Path>, - package_id: &'data str, + version_id: &'data str, dist: Option<&'data SourceDist>, build_kind: BuildKind, ) -> Result { @@ -304,7 +304,7 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.interpreter, self, self.source_build_context.clone(), - package_id.to_string(), + version_id.to_string(), self.setup_py, self.config_settings.clone(), self.build_isolation, diff --git a/crates/uv-distribution/src/index/built_wheel_index.rs b/crates/uv-distribution/src/index/built_wheel_index.rs index 16f44ff20..48777ba22 100644 --- a/crates/uv-distribution/src/index/built_wheel_index.rs +++ b/crates/uv-distribution/src/index/built_wheel_index.rs @@ -47,7 +47,7 @@ impl<'a> BuiltWheelIndex<'a> { // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. let revision = pointer.into_revision(); - if !revision.satisfies(self.hasher.get(&source_dist.name)) { + if !revision.satisfies(self.hasher.get(source_dist)) { return Ok(None); } @@ -81,7 +81,7 @@ impl<'a> BuiltWheelIndex<'a> { // Enforce hash-checking by omitting any wheels that don't satisfy the required hashes. let revision = pointer.into_revision(); - if !revision.satisfies(self.hasher.get(&source_dist.name)) { + if !revision.satisfies(self.hasher.get(source_dist)) { return Ok(None); } @@ -91,7 +91,7 @@ impl<'a> BuiltWheelIndex<'a> { /// Return the most compatible [`CachedWheel`] for a given source distribution at a git URL. pub fn git(&self, source_dist: &GitSourceDist) -> Option { // Enforce hash-checking, which isn't supported for Git distributions. - if self.hasher.get(&source_dist.name).is_validate() { + if self.hasher.get(source_dist).is_validate() { return None; } diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 29b815d1a..0149a0cfb 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -123,7 +123,7 @@ impl<'a> RegistryWheelIndex<'a> { CachedWheel::from_http_pointer(wheel_dir.join(file), cache) { // Enforce hash-checking based on the built distribution. - if wheel.satisfies(hasher.get(package)) { + if wheel.satisfies(hasher.get_package(package)) { Self::add_wheel(wheel, tags, &mut versions); } } @@ -139,7 +139,7 @@ impl<'a> RegistryWheelIndex<'a> { CachedWheel::from_local_pointer(wheel_dir.join(file), cache) { // Enforce hash-checking based on the built distribution. - if wheel.satisfies(hasher.get(package)) { + if wheel.satisfies(hasher.get_package(package)) { Self::add_wheel(wheel, tags, &mut versions); } } @@ -184,7 +184,7 @@ impl<'a> RegistryWheelIndex<'a> { if let Some(revision) = revision { // Enforce hash-checking based on the source distribution. - if revision.satisfies(hasher.get(package)) { + if revision.satisfies(hasher.get_package(package)) { for wheel_dir in symlinks(cache_shard.join(revision.id())) { if let Some(wheel) = CachedWheel::from_built_source(wheel_dir) { Self::add_wheel(wheel, tags, &mut versions); diff --git a/crates/uv-installer/src/downloader.rs b/crates/uv-installer/src/downloader.rs index 5862bb210..e83d0bec8 100644 --- a/crates/uv-installer/src/downloader.rs +++ b/crates/uv-installer/src/downloader.rs @@ -8,7 +8,7 @@ use tracing::instrument; use url::Url; use distribution_types::{ - BuildableSource, CachedDist, Dist, Hashed, Identifier, LocalEditable, LocalEditables, Name, + BuildableSource, CachedDist, Dist, Hashed, Identifier, LocalEditable, LocalEditables, RemoteSource, }; use platform_tags::Tags; @@ -170,7 +170,7 @@ impl<'a, Context: BuildContext + Send + Sync> Downloader<'a, Context> { pub async fn get_wheel(&self, dist: Dist, in_flight: &InFlight) -> Result { let id = dist.distribution_id(); if in_flight.downloads.register(id.clone()) { - let policy = self.hashes.get(dist.name()); + let policy = self.hashes.get(&dist); let result = self .database .get_or_build_wheel(&dist, self.tags, policy) diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index 9895ac3be..21970eafb 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -13,7 +13,6 @@ use distribution_types::{ use pep508_rs::{Requirement, VersionOrUrl}; use platform_tags::Tags; use uv_cache::{ArchiveTarget, ArchiveTimestamp, Cache, CacheBucket, WheelCache}; - use uv_configuration::{NoBinary, Reinstall}; use uv_distribution::{ BuiltWheelIndex, HttpArchivePointer, LocalArchivePointer, RegistryWheelIndex, @@ -259,7 +258,7 @@ impl<'a> Planner<'a> { // Read the HTTP pointer. if let Some(pointer) = HttpArchivePointer::read_from(&cache_entry)? { let archive = pointer.into_archive(); - if archive.satisfies(hasher.get(&requirement.name)) { + if archive.satisfies(hasher.get(&wheel)) { let cached_dist = CachedDirectUrlDist::from_url( wheel.filename, wheel.url, @@ -301,7 +300,7 @@ impl<'a> Planner<'a> { let timestamp = ArchiveTimestamp::from_file(&wheel.path)?; if pointer.is_up_to_date(timestamp) { let archive = pointer.into_archive(); - if archive.satisfies(hasher.get(&requirement.name)) { + if archive.satisfies(hasher.get(&wheel)) { let cached_dist = CachedDirectUrlDist::from_url( wheel.filename, wheel.url, diff --git a/crates/uv-requirements/src/lookahead.rs b/crates/uv-requirements/src/lookahead.rs index 7d8ae63bf..496b6506a 100644 --- a/crates/uv-requirements/src/lookahead.rs +++ b/crates/uv-requirements/src/lookahead.rs @@ -5,7 +5,7 @@ use futures::stream::FuturesUnordered; use futures::StreamExt; use rustc_hash::FxHashSet; -use distribution_types::{Dist, DistributionMetadata, LocalEditable, Name}; +use distribution_types::{Dist, DistributionMetadata, LocalEditable}; use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; use pypi_types::Metadata23; use uv_client::RegistryClient; @@ -138,7 +138,7 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { // Fetch the metadata for the distribution. let requires_dist = { - let id = dist.package_id(); + let id = dist.version_id(); if let Some(archive) = self .index .get_metadata(&id) @@ -157,7 +157,7 @@ impl<'a, Context: BuildContext + Send + Sync> LookaheadResolver<'a, Context> { // Run the PEP 517 build process to extract metadata from the source distribution. let archive = self .database - .get_or_build_wheel_metadata(&dist, self.hasher.get(dist.name())) + .get_or_build_wheel_metadata(&dist, self.hasher.get(&dist)) .await .with_context(|| match &dist { Dist::Built(built) => format!("Failed to download: {built}"), diff --git a/crates/uv-requirements/src/source_tree.rs b/crates/uv-requirements/src/source_tree.rs index 79ad535e0..920c42993 100644 --- a/crates/uv-requirements/src/source_tree.rs +++ b/crates/uv-requirements/src/source_tree.rs @@ -5,7 +5,7 @@ use anyhow::{Context, Result}; use futures::{StreamExt, TryStreamExt}; use url::Url; -use distribution_types::{BuildableSource, HashPolicy, PackageId, PathSourceUrl, SourceUrl}; +use distribution_types::{BuildableSource, HashPolicy, PathSourceUrl, SourceUrl, VersionId}; use pep508_rs::Requirement; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, Reporter}; @@ -92,8 +92,7 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { let hashes = match self.hasher { HashStrategy::None => HashPolicy::None, HashStrategy::Generate => HashPolicy::Generate, - HashStrategy::Validate(_) => { - // TODO(charlie): Support `--require-hashes` for unnamed requirements. + HashStrategy::Validate { .. } => { return Err(anyhow::anyhow!( "Hash-checking is not supported for local directories: {}", source_tree.user_display() @@ -103,7 +102,7 @@ impl<'a, Context: BuildContext + Send + Sync> SourceTreeResolver<'a, Context> { // Fetch the metadata for the distribution. let metadata = { - let id = PackageId::from_url(source.url()); + let id = VersionId::from_url(source.url()); if let Some(archive) = self .index .get_metadata(&id) diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index deeb847e5..d51460e50 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -10,8 +10,8 @@ use tracing::debug; use distribution_filename::{SourceDistFilename, WheelFilename}; use distribution_types::{ - BuildableSource, DirectSourceUrl, GitSourceUrl, HashPolicy, PackageId, PathSourceUrl, - RemoteSource, SourceUrl, + BuildableSource, DirectSourceUrl, GitSourceUrl, PathSourceUrl, RemoteSource, SourceUrl, + VersionId, }; use pep508_rs::{ Requirement, RequirementsTxtRequirement, Scheme, UnnamedRequirement, VersionOrUrl, @@ -241,7 +241,7 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont // Fetch the metadata for the distribution. let name = { - let id = PackageId::from_url(source.url()); + let id = VersionId::from_url(source.url()); if let Some(archive) = index.get_metadata(&id).as_deref().and_then(|response| { if let MetadataResponse::Found(archive) = response { Some(archive) @@ -252,20 +252,8 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont // If the metadata is already in the index, return it. archive.metadata.name.clone() } else { - // Determine the hash policy. Since we don't have a package name, we perform a - // manual match. - let hashes = match hasher { - HashStrategy::None => HashPolicy::None, - HashStrategy::Generate => HashPolicy::Generate, - HashStrategy::Validate(_) => { - // TODO(charlie): Support `--require-hashes` for unnamed requirements. - return Err(anyhow::anyhow!( - "Unnamed requirements are not supported with `--require-hashes`" - )); - } - }; - // Run the PEP 517 build process to extract metadata from the source distribution. + let hashes = hasher.get_url(source.url()); let source = BuildableSource::Url(source); let archive = database.build_wheel_metadata(&source, hashes).await?; diff --git a/crates/uv-resolver/src/flat_index.rs b/crates/uv-resolver/src/flat_index.rs index 4a0093471..f37577c85 100644 --- a/crates/uv-resolver/src/flat_index.rs +++ b/crates/uv-resolver/src/flat_index.rs @@ -132,7 +132,7 @@ impl FlatIndex { } // Check if hashes line up - let hash = if let HashPolicy::Validate(required) = hasher.get(&filename.name) { + let hash = if let HashPolicy::Validate(required) = hasher.get_package(&filename.name) { if hashes.is_empty() { Hash::Missing } else if required.iter().any(|hash| hashes.contains(hash)) { @@ -174,7 +174,7 @@ impl FlatIndex { }; // Check if hashes line up - let hash = if let HashPolicy::Validate(required) = hasher.get(&filename.name) { + let hash = if let HashPolicy::Validate(required) = hasher.get_package(&filename.name) { if hashes.is_empty() { Hash::Missing } else if required.iter().any(|hash| hashes.contains(hash)) { diff --git a/crates/uv-resolver/src/resolution.rs b/crates/uv-resolver/src/resolution.rs index 7203668b1..533a979b0 100644 --- a/crates/uv-resolver/src/resolution.rs +++ b/crates/uv-resolver/src/resolution.rs @@ -12,7 +12,7 @@ use pubgrub::type_aliases::SelectedDependencies; use rustc_hash::{FxHashMap, FxHashSet}; use distribution_types::{ - Dist, DistributionMetadata, IndexUrl, LocalEditable, Name, PackageId, ResolvedDist, Verbatim, + Dist, DistributionMetadata, IndexUrl, LocalEditable, Name, ResolvedDist, Verbatim, VersionId, VersionOrUrl, }; use once_map::OnceMap; @@ -66,7 +66,7 @@ impl ResolutionGraph { selection: &SelectedDependencies, pins: &FilePins, packages: &OnceMap, - distributions: &OnceMap, + distributions: &OnceMap, state: &State, preferences: &Preferences, editables: Editables, @@ -135,7 +135,7 @@ impl ResolutionGraph { { hashes.insert(package_name.clone(), digests.to_vec()); } else if let Some(metadata_response) = - distributions.get(&pinned_package.package_id()) + distributions.get(&pinned_package.version_id()) { if let MetadataResponse::Found(ref archive) = *metadata_response { let mut digests = archive.hashes.clone(); @@ -168,17 +168,17 @@ impl ResolutionGraph { }); } } else { - let response = distributions.get(&dist.package_id()).unwrap_or_else(|| { + let response = distributions.get(&dist.version_id()).unwrap_or_else(|| { panic!( "Every package should have metadata: {:?}", - dist.package_id() + dist.version_id() ) }); let MetadataResponse::Found(archive) = &*response else { panic!( "Every package should have metadata: {:?}", - dist.package_id() + dist.version_id() ) }; @@ -222,17 +222,17 @@ impl ResolutionGraph { }); } } else { - let response = distributions.get(&dist.package_id()).unwrap_or_else(|| { + let response = distributions.get(&dist.version_id()).unwrap_or_else(|| { panic!( "Every package should have metadata: {:?}", - dist.package_id() + dist.version_id() ) }); let MetadataResponse::Found(archive) = &*response else { panic!( "Every package should have metadata: {:?}", - dist.package_id() + dist.version_id() ) }; @@ -429,20 +429,20 @@ impl ResolutionGraph { let mut seen_marker_values = FxHashSet::default(); for i in self.petgraph.node_indices() { let dist = &self.petgraph[i]; - let package_id = match dist.version_or_url() { + let version_id = match dist.version_or_url() { VersionOrUrl::Version(version) => { - PackageId::from_registry(dist.name().clone(), version.clone()) + VersionId::from_registry(dist.name().clone(), version.clone()) } - VersionOrUrl::Url(verbatim_url) => PackageId::from_url(verbatim_url.raw()), + VersionOrUrl::Url(verbatim_url) => VersionId::from_url(verbatim_url.raw()), }; let res = index .distributions - .get(&package_id) + .get(&version_id) .expect("every package in resolution graph has metadata"); let MetadataResponse::Found(archive, ..) = &*res else { panic!( "Every package should have metadata: {:?}", - dist.package_id() + dist.version_id() ) }; for req in manifest.apply(&archive.metadata.requires_dist) { diff --git a/crates/uv-resolver/src/resolver/batch_prefetch.rs b/crates/uv-resolver/src/resolver/batch_prefetch.rs index 93d32c4fa..558cdc2b7 100644 --- a/crates/uv-resolver/src/resolver/batch_prefetch.rs +++ b/crates/uv-resolver/src/resolver/batch_prefetch.rs @@ -141,7 +141,7 @@ impl BatchPrefetcher { dist ); prefetch_count += 1; - if index.distributions.register(candidate.package_id()) { + if index.distributions.register(candidate.version_id()) { let request = match dist { ResolvedDistRef::Installable(dist) => Request::Dist(dist.clone()), ResolvedDistRef::Installed(dist) => Request::Installed(dist.clone()), diff --git a/crates/uv-resolver/src/resolver/index.rs b/crates/uv-resolver/src/resolver/index.rs index af42c9a6f..614baf206 100644 --- a/crates/uv-resolver/src/resolver/index.rs +++ b/crates/uv-resolver/src/resolver/index.rs @@ -1,6 +1,6 @@ use std::sync::Arc; -use distribution_types::PackageId; +use distribution_types::VersionId; use once_map::OnceMap; use uv_normalize::PackageName; @@ -14,7 +14,7 @@ pub struct InMemoryIndex { pub(crate) packages: OnceMap, /// A map from package ID to metadata for that distribution. - pub(crate) distributions: OnceMap, + pub(crate) distributions: OnceMap, } impl InMemoryIndex { @@ -24,8 +24,8 @@ impl InMemoryIndex { } /// Insert a [`Metadata23`] into the index. - pub fn insert_metadata(&self, package_id: PackageId, response: MetadataResponse) { - self.distributions.done(package_id, response); + pub fn insert_metadata(&self, version_id: VersionId, response: MetadataResponse) { + self.distributions.done(version_id, response); } /// Get the [`VersionsResponse`] for a given package name, without waiting. @@ -34,7 +34,7 @@ impl InMemoryIndex { } /// Get the [`MetadataResponse`] for a given package ID, without waiting. - pub fn get_metadata(&self, package_id: &PackageId) -> Option> { - self.distributions.get(package_id) + pub fn get_metadata(&self, version_id: &VersionId) -> Option> { + self.distributions.get(version_id) } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 2ddf6c365..74a564682 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -523,29 +523,27 @@ impl< match package { PubGrubPackage::Root(_) => {} PubGrubPackage::Python(_) => {} - PubGrubPackage::Package(package_name, _extra, None) => { - // Validate that the package is permitted under hash-checking mode. - if !self.hasher.allows(package_name) { - return Err(ResolveError::UnhashedPackage(package_name.clone())); + PubGrubPackage::Package(name, _extra, None) => { + // Verify that the package is allowed under the hash-checking policy. + if !self.hasher.allows_package(name) { + return Err(ResolveError::UnhashedPackage(name.clone())); } // Emit a request to fetch the metadata for this package. - if self.index.packages.register(package_name.clone()) { - priorities.add(package_name.clone()); - request_sink - .send(Request::Package(package_name.clone())) - .await?; + if self.index.packages.register(name.clone()) { + priorities.add(name.clone()); + request_sink.send(Request::Package(name.clone())).await?; } } - PubGrubPackage::Package(package_name, _extra, Some(url)) => { - // Validate that the package is permitted under hash-checking mode. - if !self.hasher.allows(package_name) { - return Err(ResolveError::UnhashedPackage(package_name.clone())); + PubGrubPackage::Package(name, _extra, Some(url)) => { + // Verify that the package is allowed under the hash-checking policy. + if !self.hasher.allows_url(url) { + return Err(ResolveError::UnhashedPackage(name.clone())); } // Emit a request to fetch the metadata for this distribution. - let dist = Dist::from_url(package_name.clone(), url.clone())?; - if self.index.distributions.register(dist.package_id()) { + let dist = Dist::from_url(name.clone(), url.clone())?; + if self.index.distributions.register(dist.version_id()) { priorities.add(dist.name().clone()); request_sink.send(Request::Dist(dist)).await?; } @@ -646,7 +644,7 @@ impl< let response = self .index .distributions - .wait(&dist.package_id()) + .wait(&dist.version_id()) .await .ok_or(ResolveError::Unregistered)?; @@ -796,7 +794,7 @@ impl< let version = candidate.version().clone(); // Emit a request to fetch the metadata for this version. - if self.index.distributions.register(candidate.package_id()) { + if self.index.distributions.register(candidate.version_id()) { let request = match dist.for_resolution() { ResolvedDistRef::Installable(dist) => Request::Dist(dist.clone()), ResolvedDistRef::Installed(dist) => Request::Installed(dist.clone()), @@ -880,13 +878,13 @@ impl< Some(url) => PubGrubDistribution::from_url(package_name, url), None => PubGrubDistribution::from_registry(package_name, version), }; - let package_id = dist.package_id(); + let version_id = dist.version_id(); // Wait for the metadata to be available. self.index .distributions - .wait(&package_id) - .instrument(info_span!("distributions_wait", %package_id)) + .wait(&version_id) + .instrument(info_span!("distributions_wait", %version_id)) .await .ok_or(ResolveError::Unregistered)?; } @@ -931,7 +929,7 @@ impl< Some(url) => PubGrubDistribution::from_url(package_name, url), None => PubGrubDistribution::from_registry(package_name, version), }; - let package_id = dist.package_id(); + let version_id = dist.version_id(); // If the package does not exist in the registry or locally, we cannot fetch its dependencies if self.unavailable_packages.get(package_name).is_some() @@ -953,8 +951,8 @@ impl< let response = self .index .distributions - .wait(&package_id) - .instrument(info_span!("distributions_wait", %package_id)) + .wait(&version_id) + .instrument(info_span!("distributions_wait", %version_id)) .await .ok_or(ResolveError::Unregistered)?; @@ -1061,7 +1059,7 @@ impl< Some(Response::Installed { dist, metadata }) => { trace!("Received installed distribution metadata for: {dist}"); self.index.distributions.done( - dist.package_id(), + dist.version_id(), MetadataResponse::Found(ArchiveMetadata::from(metadata)), ); } @@ -1079,7 +1077,7 @@ impl< } _ => {} } - self.index.distributions.done(dist.package_id(), metadata); + self.index.distributions.done(dist.version_id(), metadata); } Some(Response::Dist { dist: Dist::Source(dist), @@ -1095,7 +1093,7 @@ impl< } _ => {} } - self.index.distributions.done(dist.package_id(), metadata); + self.index.distributions.done(dist.version_id(), metadata); } None => {} } @@ -1200,7 +1198,7 @@ impl< }; // Emit a request to fetch the metadata for this version. - if self.index.distributions.register(candidate.package_id()) { + if self.index.distributions.register(candidate.version_id()) { let dist = dist.for_resolution().to_owned(); let response = match dist { diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index 566da4894..3479620ac 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -3,7 +3,7 @@ use std::future::Future; use anyhow::Result; use chrono::{DateTime, Utc}; -use distribution_types::{Dist, IndexLocations, Name}; +use distribution_types::{Dist, IndexLocations}; use platform_tags::Tags; use uv_client::RegistryClient; @@ -181,7 +181,7 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider async fn get_or_build_wheel_metadata<'io>(&'io self, dist: &'io Dist) -> WheelMetadataResult { match self .fetcher - .get_or_build_wheel_metadata(dist, self.hasher.get(dist.name())) + .get_or_build_wheel_metadata(dist, self.hasher.get(dist)) .await { Ok(metadata) => Ok(MetadataResponse::Found(metadata)), diff --git a/crates/uv-resolver/src/version_map.rs b/crates/uv-resolver/src/version_map.rs index e6309f00c..91e41c4e2 100644 --- a/crates/uv-resolver/src/version_map.rs +++ b/crates/uv-resolver/src/version_map.rs @@ -112,7 +112,7 @@ impl VersionMap { .allowed_versions(package_name) .cloned() .unwrap_or_default(); - let required_hashes = hasher.get(package_name).digests().to_vec(); + let required_hashes = hasher.get_package(package_name).digests().to_vec(); Self { inner: VersionMapInner::Lazy(VersionMapLazy { map, diff --git a/crates/uv-types/Cargo.toml b/crates/uv-types/Cargo.toml index 2ee16ce44..c517c0bf0 100644 --- a/crates/uv-types/Cargo.toml +++ b/crates/uv-types/Cargo.toml @@ -30,6 +30,7 @@ rustc-hash = { workspace = true } serde = { workspace = true, optional = true } serde_json = { workspace = true, optional = true } thiserror = { workspace = true } +url = { workspace = true } [features] default = [] diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index 4e0e8532d..592682998 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -1,8 +1,10 @@ -use distribution_types::HashPolicy; -use rustc_hash::FxHashMap; use std::str::FromStr; -use pep508_rs::{MarkerEnvironment, Requirement, VersionOrUrl}; +use rustc_hash::FxHashMap; +use url::Url; + +use distribution_types::{DistributionMetadata, HashPolicy, PackageId}; +use pep508_rs::{MarkerEnvironment, RequirementsTxtRequirement, VersionOrUrl}; use pypi_types::{HashDigest, HashError}; use uv_normalize::PackageName; @@ -14,74 +16,115 @@ pub enum HashStrategy { Generate, /// Hashes should be validated against a pre-defined list of hashes. If necessary, hashes should /// be generated so as to ensure that the archive is valid. - Validate(FxHashMap>), + Validate(FxHashMap>), } impl HashStrategy { - /// Return the [`HashPolicy`] for the given package. - pub fn get(&self, package_name: &PackageName) -> HashPolicy { + /// Return the [`HashPolicy`] for the given distribution. + pub fn get(&self, distribution: &T) -> HashPolicy { match self { Self::None => HashPolicy::None, Self::Generate => HashPolicy::Generate, Self::Validate(hashes) => hashes - .get(package_name) + .get(&distribution.package_id()) .map(Vec::as_slice) .map_or(HashPolicy::None, HashPolicy::Validate), } } - /// Returns `true` if the given package is allowed. - pub fn allows(&self, package_name: &PackageName) -> bool { + /// Return the [`HashPolicy`] for the given registry-based package. + pub fn get_package(&self, name: &PackageName) -> HashPolicy { match self { - Self::None => true, - Self::Generate => true, - Self::Validate(hashes) => hashes.contains_key(package_name), + Self::None => HashPolicy::None, + Self::Generate => HashPolicy::Generate, + Self::Validate(hashes) => hashes + .get(&PackageId::from_registry(name.clone())) + .map(Vec::as_slice) + .map_or(HashPolicy::None, HashPolicy::Validate), } } - /// Generate the required hashes from a set of [`Requirement`] entries. + /// Return the [`HashPolicy`] for the given direct URL package. + pub fn get_url(&self, url: &Url) -> HashPolicy { + match self { + Self::None => HashPolicy::None, + Self::Generate => HashPolicy::Generate, + Self::Validate(hashes) => hashes + .get(&PackageId::from_url(url)) + .map(Vec::as_slice) + .map_or(HashPolicy::None, HashPolicy::Validate), + } + } + + /// Returns `true` if the given registry-based package is allowed. + pub fn allows_package(&self, name: &PackageName) -> bool { + match self { + Self::None => true, + Self::Generate => true, + Self::Validate(hashes) => hashes.contains_key(&PackageId::from_registry(name.clone())), + } + } + + /// Returns `true` if the given direct URL package is allowed. + pub fn allows_url(&self, url: &Url) -> bool { + match self { + Self::None => true, + Self::Generate => true, + Self::Validate(hashes) => hashes.contains_key(&PackageId::from_url(url)), + } + } + + /// Generate the required hashes from a set of [`RequirementsTxtRequirement`] entries. pub fn from_requirements( - requirements: impl Iterator)>, + requirements: impl Iterator)>, markers: &MarkerEnvironment, ) -> Result { - let mut hashes = FxHashMap::>::default(); + let mut hashes = FxHashMap::>::default(); // For each requirement, map from name to allowed hashes. We use the last entry for each // package. - // - // For now, unnamed requirements are unsupported. This should be fine, since `--require-hashes` - // tends to be used after `pip-compile`, which will always output named requirements. - // - // TODO(charlie): Preserve hashes from `requirements.txt` through to this pass, so that we - // can iterate over requirements directly, rather than iterating over the entries. for (requirement, digests) in requirements { if !requirement.evaluate_markers(markers, &[]) { continue; } // Every requirement must be either a pinned version or a direct URL. - match requirement.version_or_url.as_ref() { - Some(VersionOrUrl::Url(_)) => { - // Direct URLs are always allowed. - } - Some(VersionOrUrl::VersionSpecifier(specifiers)) => { - if specifiers - .iter() - .any(|specifier| matches!(specifier.operator(), pep440_rs::Operator::Equal)) - { - // Pinned versions are allowed. - } else { - return Err(HashStrategyError::UnpinnedRequirement( - requirement.to_string(), - )); + let id = match &requirement { + RequirementsTxtRequirement::Pep508(requirement) => { + match requirement.version_or_url.as_ref() { + Some(VersionOrUrl::Url(url)) => { + // Direct URLs are always allowed. + PackageId::from_url(url) + } + Some(VersionOrUrl::VersionSpecifier(specifiers)) => { + // Must be a single specifier. + let [specifier] = specifiers.as_ref() else { + return Err(HashStrategyError::UnpinnedRequirement( + requirement.to_string(), + )); + }; + + // Must be pinned to a specific version. + if *specifier.operator() != pep440_rs::Operator::Equal { + return Err(HashStrategyError::UnpinnedRequirement( + requirement.to_string(), + )); + } + + PackageId::from_registry(requirement.name.clone()) + } + None => { + return Err(HashStrategyError::UnpinnedRequirement( + requirement.to_string(), + )) + } } } - None => { - return Err(HashStrategyError::UnpinnedRequirement( - requirement.to_string(), - )) + RequirementsTxtRequirement::Unnamed(requirement) => { + // Direct URLs are always allowed. + PackageId::from_url(&requirement.url) } - } + }; // Every requirement must include a hash. if digests.is_empty() { @@ -95,8 +138,7 @@ impl HashStrategy { .collect::, _>>() .unwrap(); - // TODO(charlie): Extract hashes from URL fragments. - hashes.insert(requirement.name, digests); + hashes.insert(id, digests); } Ok(Self::Validate(hashes)) @@ -107,8 +149,6 @@ impl HashStrategy { pub enum HashStrategyError { #[error(transparent)] Hash(#[from] HashError), - #[error("Unnamed requirements are not supported in `--require-hashes`")] - UnnamedRequirement, #[error("In `--require-hashes` mode, all requirement must have their versions pinned with `==`, but found: {0}")] UnpinnedRequirement(String), #[error("In `--require-hashes` mode, all requirement must have a hash, but none were provided for: {0}")] diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index 62b2ea818..bd5498f52 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -95,13 +95,13 @@ pub trait BuildContext: Sync { /// /// For PEP 517 builds, this calls `get_requires_for_build_wheel`. /// - /// `package_id` is for error reporting only. + /// `version_id` is for error reporting only. /// `dist` is for safety checks and may be null for editable builds. fn setup_build<'a>( &'a self, source: &'a Path, subdirectory: Option<&'a Path>, - package_id: &'a str, + version_id: &'a str, dist: Option<&'a SourceDist>, build_kind: BuildKind, ) -> impl Future> + Send + 'a; diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index cffa3d8ea..b2389d8e0 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -15,7 +15,7 @@ use distribution_types::{ LocalEditables, Name, Resolution, }; use install_wheel_rs::linker::LinkMode; -use pep508_rs::{MarkerEnvironment, Requirement, RequirementsTxtRequirement}; +use pep508_rs::{MarkerEnvironment, Requirement}; use platform_tags::Tags; use pypi_types::{Metadata23, Yanked}; use requirements_txt::EditableRequirement; @@ -191,10 +191,7 @@ pub(crate) async fn pip_install( HashStrategy::from_requirements( entries .into_iter() - .filter_map(|requirement| match requirement.requirement { - RequirementsTxtRequirement::Pep508(req) => Some((req, requirement.hashes)), - RequirementsTxtRequirement::Unnamed(_) => None, - }), + .map(|entry| (entry.requirement, entry.hashes)), markers, )? } else { diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index 2d401a646..b2295c1e7 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -10,7 +10,7 @@ use distribution_types::{ IndexLocations, InstalledMetadata, LocalDist, LocalEditable, LocalEditables, Name, ResolvedDist, }; use install_wheel_rs::linker::LinkMode; -use pep508_rs::RequirementsTxtRequirement; + use platform_tags::Tags; use pypi_types::Yanked; use requirements_txt::EditableRequirement; @@ -140,10 +140,7 @@ pub(crate) async fn pip_sync( HashStrategy::from_requirements( entries .into_iter() - .filter_map(|requirement| match requirement.requirement { - RequirementsTxtRequirement::Pep508(req) => Some((req, requirement.hashes)), - RequirementsTxtRequirement::Unnamed(_) => None, - }), + .map(|entry| (entry.requirement, entry.hashes)), markers, )? } else { diff --git a/crates/uv/tests/pip_sync.rs b/crates/uv/tests/pip_sync.rs index d39c103e2..811a681f5 100644 --- a/crates/uv/tests/pip_sync.rs +++ b/crates/uv/tests/pip_sync.rs @@ -3855,24 +3855,29 @@ fn require_hashes_source_path_mismatch() -> Result<()> { Ok(()) } -/// `--require-hashes` isn't supported for unnamed requirements (yet). +/// We allow `--require-hashes` for direct URL dependencies. #[test] fn require_hashes_unnamed() -> Result<()> { let context = TestContext::new("3.12"); let requirements_txt = context.temp_dir.child("requirements.txt"); requirements_txt - .write_str("https://foo.com --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f")?; + .write_str(indoc::indoc! {r" + https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + "} )?; uv_snapshot!(command(&context) .arg("requirements.txt") .arg("--require-hashes"), @r###" - success: false - exit_code: 2 + success: true + exit_code: 0 ----- stdout ----- ----- stderr ----- - error: Unnamed requirements are not supported with `--require-hashes` + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) "### ); From 8e5a40e33ce711ad9cfcb15440e4d0ab58a63f1d Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 11:42:59 -0400 Subject: [PATCH 105/110] Add additional coverage for `--require-hashes` in `install` (#2994) --- crates/uv/tests/pip_install.rs | 160 +++++++++++++++++++++++++++++++++ 1 file changed, 160 insertions(+) diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 0832912a3..95457a472 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -3739,6 +3739,81 @@ fn find_links_no_binary() -> Result<()> { Ok(()) } +/// Provide valid hashes for all dependencies with `--require-hashes`. +#[test] +fn require_hashes() -> Result<()> { + let context = TestContext::new("3.12"); + + // Write to a requirements file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc::indoc! {r" + anyio==4.0.0 \ + --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f \ + --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + "})?; + + uv_snapshot!(context.install() + .arg("-r") + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.0.0 + + idna==3.6 + + sniffio==1.3.1 + "### + ); + + Ok(()) +} + +/// Omit hashes for dependencies with `--require-hashes`, which is allowed with `--no-deps`. +#[test] +fn require_hashes_no_deps() -> Result<()> { + let context = TestContext::new("3.12"); + + // Write to a requirements file. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt.write_str(indoc::indoc! {r" + anyio==4.0.0 \ + --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f \ + --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + "})?; + + uv_snapshot!(context.install() + .arg("-r") + .arg("requirements.txt") + .arg("--no-deps") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 1 package in [TIME] + Downloaded 1 package in [TIME] + Installed 1 package in [TIME] + + anyio==4.0.0 + "### + ); + + Ok(()) +} + /// Provide the wrong hash with `--require-hashes`. #[test] fn require_hashes_mismatch() -> Result<()> { @@ -3881,6 +3956,91 @@ fn require_hashes_constraint() -> Result<()> { Ok(()) } +/// We allow `--require-hashes` for unnamed URL dependencies. +#[test] +fn require_hashes_unnamed() -> Result<()> { + let context = TestContext::new("3.12"); + + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str(indoc::indoc! {r" + https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + "})?; + + uv_snapshot!(context.install() + .arg("-r") + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + + idna==3.6 + + sniffio==1.3.1 + "### + ); + + Ok(()) +} + +/// We allow `--require-hashes` for unnamed URL dependencies. In this case, the unnamed URL is +/// a repeat of a registered package. +#[test] +fn require_hashes_unnamed_repeated() -> Result<()> { + let context = TestContext::new("3.12"); + + // Re-run, but duplicate `anyio`. + let requirements_txt = context.temp_dir.child("requirements.txt"); + requirements_txt + .write_str(indoc::indoc! {r" + anyio==4.0.0 \ + --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f \ + --hash=sha256:f7ed51751b2c2add651e5747c891b47e26d2a21be5d32d9311dfe9692f3e5d7a + https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl --hash=sha256:cfdb2b588b9fc25ede96d8db56ed50848b0b649dca3dd1df0b11f683bb9e0b5f + idna==3.6 \ + --hash=sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca \ + --hash=sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f + # via anyio + sniffio==1.3.1 \ + --hash=sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2 \ + --hash=sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc + # via anyio + "} )?; + + uv_snapshot!(context.install() + .arg("-r") + .arg("requirements.txt") + .arg("--require-hashes"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Downloaded 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==4.0.0 (from https://files.pythonhosted.org/packages/36/55/ad4de788d84a630656ece71059665e01ca793c04294c463fd84132f40fe6/anyio-4.0.0-py3-none-any.whl) + + idna==3.6 + + sniffio==1.3.1 + "### + ); + + Ok(()) +} + /// If a hash is only included as a override, that's not good enough for `--require-hashes`. /// /// TODO(charlie): This _should_ be allowed. It's a bug. From d03e9f2b8c52e0eb70ceaec6b0df451abbbad948 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 11:58:00 -0400 Subject: [PATCH 106/110] Add `UV_BREAK_SYSTEM_PACKAGES` environment variable (#2995) ## Summary Requested here: https://github.com/astral-sh/uv/issues/2988. Seems reasonable to me given that pip supports it and we already have `UV_SYSTEM_PYTHON`. Closes https://github.com/astral-sh/uv/issues/2988 --- README.md | 13 ++++++++++--- crates/uv/src/main.rs | 6 +++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index 74ba5ef93..a41081cc4 100644 --- a/README.md +++ b/README.md @@ -453,10 +453,17 @@ uv accepts the following command-line arguments as environment variables: `lowest-direct`, uv will install the lowest compatible versions of all direct dependencies. - `UV_PRERELEASE`: Equivalent to the `--prerelease` command-line argument. For example, if set to `allow`, uv will allow pre-release versions for all dependencies. -- `UV_SYSTEM_PYTHON`: Equivalent to the `--system` command-line argument. If set to `true`, uv +- `UV_SYSTEM_PYTHON`: Equivalent to the `--system` command-line argument. If set to `true`, uv will use the first Python interpreter found in the system `PATH`. - WARNING: `UV_SYSTEM_PYTHON=true` is intended for use in continuous integration (CI) environments and - should be used with caution, as it can modify the system Python installation. + WARNING: `UV_SYSTEM_PYTHON=true` is intended for use in continuous integration (CI) or + containerized environments and should be used with caution, as modifying the system Python + can lead to unexpected behavior. +- `UV_BREAK_SYSTEM_PACKAGES`: Equivalent to the `--break-system-packages` command-line argument. If + set to `true`, uv will allow the installation of packages that conflict with system-installed + packages. + WARNING: `UV_BREAK_SYSTEM_PACKAGES=true` is intended for use in continuous integration (CI) or + containerized environments and should be used with caution, as modifying the system Python + can lead to unexpected behavior. - `UV_NATIVE_TLS`: Equivalent to the `--native-tls` command-line argument. If set to `true`, uv will use the system's trust store instead of the bundled `webpki-roots` crate. - `UV_INDEX_STRATEGY`: Equivalent to the `--index-strategy` command-line argument. For example, if diff --git a/crates/uv/src/main.rs b/crates/uv/src/main.rs index f20674194..d39b7198c 100644 --- a/crates/uv/src/main.rs +++ b/crates/uv/src/main.rs @@ -657,7 +657,7 @@ struct PipSyncArgs { /// 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`). - #[clap(long, requires = "discovery")] + #[clap(long, env = "UV_BREAK_SYSTEM_PACKAGES", requires = "discovery")] break_system_packages: bool, /// Use legacy `setuptools` behavior when building source distributions without a @@ -941,7 +941,7 @@ struct PipInstallArgs { /// 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`). - #[clap(long, requires = "discovery")] + #[clap(long, env = "UV_BREAK_SYSTEM_PACKAGES", requires = "discovery")] break_system_packages: bool, /// Use legacy `setuptools` behavior when building source distributions without a @@ -1086,7 +1086,7 @@ struct PipUninstallArgs { /// 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`). - #[clap(long, requires = "discovery")] + #[clap(long, env = "UV_BREAK_SYSTEM_PACKAGES", requires = "discovery")] break_system_packages: bool, /// Run offline, i.e., without accessing the network. From 9d5467dc2f6fbeda67569b8604b928d8d85cd60b Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 13:31:34 -0400 Subject: [PATCH 107/110] Remove outdated comment on `IndexLocations` (#2996) Closes https://github.com/astral-sh/uv/issues/2990. --- crates/distribution-types/src/index_url.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/crates/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index 5de44451e..3b0893fc2 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -171,15 +171,7 @@ impl Display for FlatIndexLocation { } } -/// The index locations to use for fetching packages. -/// -/// By default, uses the PyPI index. -/// -/// "pip treats all package sources equally" (), -/// and so do we, i.e., you can't rely that on any particular order of querying indices. -/// -/// If the fields are none and empty, ignore the package index, instead rely on local archives and -/// caches. +/// The index locations to use for fetching packages. By default, uses the PyPI index. /// /// From a pip perspective, this type merges `--index-url`, `--extra-index-url`, and `--find-links`. #[derive(Debug, Clone)] @@ -344,7 +336,9 @@ impl<'a> IndexUrls { } } - /// Return an iterator over all [`IndexUrl`] entries. + /// Return an iterator over all [`IndexUrl`] entries in order. + /// + /// Prioritizes the extra indexes over the main index. /// /// If `no_index` was enabled, then this always returns an empty /// iterator. From 0d62e62fb776c34a19422fd65b74606c3ba0bfd7 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 13:53:34 -0400 Subject: [PATCH 108/110] Make hash-checking failure mode stricter and safer (#2997) ## Summary If there are no hashes for a given package, we now return `Validate(&[])` so that the policy is impossible to satisfy. Previously, we returned `None`, which is always satisfied. We don't really ever expect to hit this, because we detect this case in the resolver and raise a different error. But if we have a bug somewhere, it's better to fail with an error than silently let the package through. --- crates/uv-distribution/src/error.rs | 76 +++++++++++++++++++++++------ crates/uv-types/src/hash.rs | 30 +++++++----- 2 files changed, 79 insertions(+), 27 deletions(-) diff --git a/crates/uv-distribution/src/error.rs b/crates/uv-distribution/src/error.rs index d5a77f81a..271ee66b0 100644 --- a/crates/uv-distribution/src/error.rs +++ b/crates/uv-distribution/src/error.rs @@ -88,12 +88,29 @@ pub enum Error { HashExhaustion(#[source] std::io::Error), #[error("Hash mismatch for {distribution}\n\nExpected:\n{expected}\n\nComputed:\n{actual}")] - HashMismatch { + MismatchedHashes { distribution: String, expected: String, actual: String, }, + #[error( + "Hash-checking is enabled, but no hashes were provided or computed for: {distribution}" + )] + MissingHashes { distribution: String }, + + #[error("Hash-checking is enabled, but no hashes were computed for: {distribution}\n\nExpected:\n{expected}")] + MissingActualHashes { + distribution: String, + expected: String, + }, + + #[error("Hash-checking is enabled, but no hashes were provided for: {distribution}\n\nComputed:\n{actual}")] + MissingExpectedHashes { + distribution: String, + actual: String, + }, + #[error("Hash-checking is not supported for local directories: {0}")] HashesNotSupportedSourceTree(String), @@ -125,22 +142,51 @@ impl Error { expected: &[HashDigest], actual: &[HashDigest], ) -> Error { - let expected = expected - .iter() - .map(|hash| format!(" {hash}")) - .collect::>() - .join("\n"); + match (expected.is_empty(), actual.is_empty()) { + (true, true) => Self::MissingHashes { distribution }, + (true, false) => { + let actual = actual + .iter() + .map(|hash| format!(" {hash}")) + .collect::>() + .join("\n"); - let actual = actual - .iter() - .map(|hash| format!(" {hash}")) - .collect::>() - .join("\n"); + Self::MissingExpectedHashes { + distribution, + actual, + } + } + (false, true) => { + let expected = expected + .iter() + .map(|hash| format!(" {hash}")) + .collect::>() + .join("\n"); - Self::HashMismatch { - distribution, - expected, - actual, + Self::MissingActualHashes { + distribution, + expected, + } + } + (false, false) => { + let expected = expected + .iter() + .map(|hash| format!(" {hash}")) + .collect::>() + .join("\n"); + + let actual = actual + .iter() + .map(|hash| format!(" {hash}")) + .collect::>() + .join("\n"); + + Self::MismatchedHashes { + distribution, + expected, + actual, + } + } } } } diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index 592682998..def34c0f2 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -25,10 +25,12 @@ impl HashStrategy { match self { Self::None => HashPolicy::None, Self::Generate => HashPolicy::Generate, - Self::Validate(hashes) => hashes - .get(&distribution.package_id()) - .map(Vec::as_slice) - .map_or(HashPolicy::None, HashPolicy::Validate), + Self::Validate(hashes) => HashPolicy::Validate( + hashes + .get(&distribution.package_id()) + .map(Vec::as_slice) + .unwrap_or_default(), + ), } } @@ -37,10 +39,12 @@ impl HashStrategy { match self { Self::None => HashPolicy::None, Self::Generate => HashPolicy::Generate, - Self::Validate(hashes) => hashes - .get(&PackageId::from_registry(name.clone())) - .map(Vec::as_slice) - .map_or(HashPolicy::None, HashPolicy::Validate), + Self::Validate(hashes) => HashPolicy::Validate( + hashes + .get(&PackageId::from_registry(name.clone())) + .map(Vec::as_slice) + .unwrap_or_default(), + ), } } @@ -49,10 +53,12 @@ impl HashStrategy { match self { Self::None => HashPolicy::None, Self::Generate => HashPolicy::Generate, - Self::Validate(hashes) => hashes - .get(&PackageId::from_url(url)) - .map(Vec::as_slice) - .map_or(HashPolicy::None, HashPolicy::Validate), + Self::Validate(hashes) => HashPolicy::Validate( + hashes + .get(&PackageId::from_url(url)) + .map(Vec::as_slice) + .unwrap_or_default(), + ), } } From a71bd6023869908c91473eca8bdd20244cfc6540 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 17:19:11 -0400 Subject: [PATCH 109/110] Allow unnamed requirements for overrides (#2999) ## Summary This PR lifts a constraint by allowing unnamed requirements in `overrides.txt` files. --- crates/requirements-txt/src/lib.rs | 2 +- crates/uv-installer/src/site_packages.rs | 35 +++++---- crates/uv-requirements/src/specification.rs | 56 +++++--------- crates/uv-requirements/src/unnamed.rs | 9 ++- crates/uv-types/src/hash.rs | 4 +- crates/uv/src/commands/pip_compile.rs | 21 ++++-- crates/uv/src/commands/pip_install.rs | 16 +++- crates/uv/src/commands/pip_sync.rs | 7 +- crates/uv/src/commands/pip_uninstall.rs | 2 +- crates/uv/tests/pip_compile.rs | 84 +++++++++++++++++++++ 10 files changed, 164 insertions(+), 72 deletions(-) diff --git a/crates/requirements-txt/src/lib.rs b/crates/requirements-txt/src/lib.rs index 137f8b235..8c897289f 100644 --- a/crates/requirements-txt/src/lib.rs +++ b/crates/requirements-txt/src/lib.rs @@ -293,7 +293,7 @@ impl Display for EditableRequirement { /// A [Requirement] with additional metadata from the requirements.txt, currently only hashes but in /// the future also editable an similar information -#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Serialize)] +#[derive(Debug, Deserialize, Clone, Eq, PartialEq, Hash, Serialize)] pub struct RequirementEntry { /// The actual PEP 508 requirement pub requirement: RequirementsTxtRequirement, diff --git a/crates/uv-installer/src/site_packages.rs b/crates/uv-installer/src/site_packages.rs index 848d862e9..ab9ed93d7 100644 --- a/crates/uv-installer/src/site_packages.rs +++ b/crates/uv-installer/src/site_packages.rs @@ -10,7 +10,7 @@ use url::Url; use distribution_types::{InstalledDist, InstalledMetadata, InstalledVersion, Name}; use pep440_rs::{Version, VersionSpecifiers}; use pep508_rs::{Requirement, RequirementsTxtRequirement, VerbatimUrl}; -use requirements_txt::EditableRequirement; +use requirements_txt::{EditableRequirement, RequirementEntry}; use uv_cache::{ArchiveTarget, ArchiveTimestamp}; use uv_interpreter::PythonEnvironment; use uv_normalize::PackageName; @@ -296,19 +296,22 @@ impl<'a> SitePackages<'a> { /// Returns `true` if the installed packages satisfy the given requirements. pub fn satisfies( &self, - requirements: &[RequirementsTxtRequirement], + requirements: &[RequirementEntry], editables: &[EditableRequirement], constraints: &[Requirement], ) -> Result { - let mut stack = Vec::::with_capacity(requirements.len()); + let mut stack = Vec::::with_capacity(requirements.len()); let mut seen = FxHashSet::with_capacity_and_hasher(requirements.len(), BuildHasherDefault::default()); // Add the direct requirements to the queue. - for dependency in requirements { - if dependency.evaluate_markers(self.venv.interpreter().markers(), &[]) { - if seen.insert(dependency.clone()) { - stack.push(dependency.clone()); + for entry in requirements { + if entry + .requirement + .evaluate_markers(self.venv.interpreter().markers(), &[]) + { + if seen.insert(entry.clone()) { + stack.push(entry.clone()); } } } @@ -346,7 +349,10 @@ impl<'a> SitePackages<'a> { self.venv.interpreter().markers(), &requirement.extras, ) { - let dependency = RequirementsTxtRequirement::from(dependency); + let dependency = RequirementEntry { + requirement: RequirementsTxtRequirement::Pep508(dependency), + hashes: vec![], + }; if seen.insert(dependency.clone()) { stack.push(dependency); } @@ -361,8 +367,8 @@ impl<'a> SitePackages<'a> { } // Verify that all non-editable requirements are met. - while let Some(requirement) = stack.pop() { - let installed = match &requirement { + while let Some(entry) = stack.pop() { + let installed = match &entry.requirement { RequirementsTxtRequirement::Pep508(requirement) => { self.get_packages(&requirement.name) } @@ -377,7 +383,7 @@ impl<'a> SitePackages<'a> { } [distribution] => { // Validate that the installed version matches the requirement. - match requirement.version_or_url() { + match entry.requirement.version_or_url() { // Accept any installed version. None => {} @@ -463,9 +469,12 @@ impl<'a> SitePackages<'a> { for dependency in metadata.requires_dist { if dependency.evaluate_markers( self.venv.interpreter().markers(), - requirement.extras(), + entry.requirement.extras(), ) { - let dependency = RequirementsTxtRequirement::from(dependency); + let dependency = RequirementEntry { + requirement: RequirementsTxtRequirement::Pep508(dependency), + hashes: vec![], + }; if seen.insert(dependency.clone()) { stack.push(dependency); } diff --git a/crates/uv-requirements/src/specification.rs b/crates/uv-requirements/src/specification.rs index 9feb8514d..63e1f64ed 100644 --- a/crates/uv-requirements/src/specification.rs +++ b/crates/uv-requirements/src/specification.rs @@ -20,14 +20,12 @@ use crate::{ExtrasSpecification, RequirementsSource}; pub struct RequirementsSpecification { /// The name of the project specifying requirements. pub project: Option, - /// The `requirements.txt` entries for the project. - pub entries: Vec, /// The requirements for the project. - pub requirements: Vec, + pub requirements: Vec, /// The constraints for the project. pub constraints: Vec, /// The overrides for the project. - pub overrides: Vec, + pub overrides: Vec, /// Package to install as editable installs pub editables: Vec, /// The source trees from which to extract requirements. @@ -62,8 +60,10 @@ impl RequirementsSpecification { .with_context(|| format!("Failed to parse `{name}`"))?; Self { project: None, - entries: vec![], - requirements: vec![requirement], + requirements: vec![RequirementEntry { + requirement, + hashes: vec![], + }], constraints: vec![], overrides: vec![], editables: vec![], @@ -82,7 +82,6 @@ impl RequirementsSpecification { .with_context(|| format!("Failed to parse `{name}`"))?; Self { project: None, - entries: vec![], requirements: vec![], constraints: vec![], overrides: vec![], @@ -102,12 +101,7 @@ impl RequirementsSpecification { RequirementsTxt::parse(path, std::env::current_dir()?, client_builder).await?; Self { project: None, - entries: requirements_txt.requirements.clone(), - requirements: requirements_txt - .requirements - .into_iter() - .map(|entry| entry.requirement) - .collect(), + requirements: requirements_txt.requirements, constraints: requirements_txt.constraints, overrides: vec![], editables: requirements_txt.editables, @@ -153,11 +147,13 @@ impl RequirementsSpecification { { Self { project: Some(project.name), - entries: vec![], requirements: project .requirements .into_iter() - .map(RequirementsTxtRequirement::Pep508) + .map(|requirement| RequirementEntry { + requirement: RequirementsTxtRequirement::Pep508(requirement), + hashes: vec![], + }) .collect(), constraints: vec![], overrides: vec![], @@ -181,7 +177,6 @@ impl RequirementsSpecification { })?; Self { project: None, - entries: vec![], requirements: vec![], constraints: vec![], overrides: vec![], @@ -207,7 +202,6 @@ impl RequirementsSpecification { })?; Self { project: None, - entries: vec![], requirements: vec![], constraints: vec![], overrides: vec![], @@ -240,7 +234,6 @@ impl RequirementsSpecification { // a requirements file can also add constraints. for source in requirements { let source = Self::from_source(source, extras, client_builder).await?; - spec.entries.extend(source.entries); spec.requirements.extend(source.requirements); spec.constraints.extend(source.constraints); spec.overrides.extend(source.overrides); @@ -270,12 +263,12 @@ impl RequirementsSpecification { spec.no_build.extend(source.no_build); } - // Read all constraints, treating _everything_ as a constraint. The raw entries (i.e., - // hashes) are ignored, as they are not relevant for constraints. + // Read all constraints, treating both requirements _and_ constraints as constraints. + // Overrides are ignored, as are the hashes, as they are not relevant for constraints. for source in constraints { let source = Self::from_source(source, extras, client_builder).await?; - for requirement in source.requirements { - match requirement { + for entry in source.requirements { + match entry.requirement { RequirementsTxtRequirement::Pep508(requirement) => { spec.constraints.push(requirement); } @@ -287,7 +280,6 @@ impl RequirementsSpecification { } } spec.constraints.extend(source.constraints); - spec.constraints.extend(source.overrides); if let Some(index_url) = source.index_url { if let Some(existing) = spec.index_url { @@ -306,23 +298,11 @@ impl RequirementsSpecification { spec.no_build.extend(source.no_build); } - // Read all overrides, treating both requirements _and_ constraints as overrides. + // Read all overrides, treating both requirements _and_ overrides as overrides. + // Constraints are ignored. for source in overrides { let source = Self::from_source(source, extras, client_builder).await?; - for requirement in source.requirements { - match requirement { - RequirementsTxtRequirement::Pep508(requirement) => { - spec.overrides.push(requirement); - } - RequirementsTxtRequirement::Unnamed(requirement) => { - return Err(anyhow::anyhow!( - "Unnamed requirements are not allowed as overrides (found: `{requirement}`)" - )); - } - } - } - spec.entries.extend(source.entries); - spec.overrides.extend(source.constraints); + spec.overrides.extend(source.requirements); spec.overrides.extend(source.overrides); if let Some(index_url) = source.index_url { diff --git a/crates/uv-requirements/src/unnamed.rs b/crates/uv-requirements/src/unnamed.rs index d51460e50..d245b8166 100644 --- a/crates/uv-requirements/src/unnamed.rs +++ b/crates/uv-requirements/src/unnamed.rs @@ -17,6 +17,7 @@ use pep508_rs::{ Requirement, RequirementsTxtRequirement, Scheme, UnnamedRequirement, VersionOrUrl, }; use pypi_types::Metadata10; +use requirements_txt::RequirementEntry; use uv_client::RegistryClient; use uv_distribution::{DistributionDatabase, Reporter}; use uv_normalize::PackageName; @@ -26,7 +27,7 @@ use uv_types::{BuildContext, HashStrategy}; /// Like [`RequirementsSpecification`], but with concrete names for all requirements. pub struct NamedRequirementsResolver<'a, Context: BuildContext + Send + Sync> { /// The requirements for the project. - requirements: Vec, + requirements: Vec, /// Whether to check hashes for distributions. hasher: &'a HashStrategy, /// The in-memory index for resolving dependencies. @@ -38,7 +39,7 @@ pub struct NamedRequirementsResolver<'a, Context: BuildContext + Send + Sync> { impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Context> { /// Instantiate a new [`NamedRequirementsResolver`] for a given set of requirements. pub fn new( - requirements: Vec, + requirements: Vec, hasher: &'a HashStrategy, context: &'a Context, client: &'a RegistryClient, @@ -70,8 +71,8 @@ impl<'a, Context: BuildContext + Send + Sync> NamedRequirementsResolver<'a, Cont database, } = self; futures::stream::iter(requirements) - .map(|requirement| async { - match requirement { + .map(|entry| async { + match entry.requirement { RequirementsTxtRequirement::Pep508(requirement) => Ok(requirement), RequirementsTxtRequirement::Unnamed(requirement) => { Self::resolve_requirement(requirement, hasher, index, &database).await diff --git a/crates/uv-types/src/hash.rs b/crates/uv-types/src/hash.rs index def34c0f2..23b992b33 100644 --- a/crates/uv-types/src/hash.rs +++ b/crates/uv-types/src/hash.rs @@ -81,8 +81,8 @@ impl HashStrategy { } /// Generate the required hashes from a set of [`RequirementsTxtRequirement`] entries. - pub fn from_requirements( - requirements: impl Iterator)>, + pub fn from_requirements<'a>( + requirements: impl Iterator, markers: &MarkerEnvironment, ) -> Result { let mut hashes = FxHashMap::>::default(); diff --git a/crates/uv/src/commands/pip_compile.rs b/crates/uv/src/commands/pip_compile.rs index 8763806af..c14cf8906 100644 --- a/crates/uv/src/commands/pip_compile.rs +++ b/crates/uv/src/commands/pip_compile.rs @@ -101,7 +101,6 @@ pub(crate) async fn pip_compile( // Read all requirements from the provided sources. let RequirementsSpecification { project, - entries: _, requirements, constraints, overrides, @@ -229,10 +228,6 @@ pub(crate) async fn pip_compile( // Read the lockfile, if present. let preferences = read_lockfile(output_file, upgrade).await?; - // Collect constraints and overrides. - let constraints = Constraints::from_requirements(constraints); - let overrides = Overrides::from_requirements(overrides); - // Resolve the flat indexes from `--find-links`. let flat_index = { let client = FlatIndexClient::new(&client, &cache); @@ -305,6 +300,22 @@ pub(crate) async fn pip_compile( requirements }; + // Resolve the overrides from the provided sources. + let overrides = NamedRequirementsResolver::new( + overrides, + &hasher, + &build_dispatch, + &client, + &top_level_index, + ) + .with_reporter(ResolverReporter::from(printer)) + .resolve() + .await?; + + // Collect constraints and overrides. + let constraints = Constraints::from_requirements(constraints); + let overrides = Overrides::from_requirements(overrides); + // Build the editables and add their requirements let editables = if editables.is_empty() { Vec::new() diff --git a/crates/uv/src/commands/pip_install.rs b/crates/uv/src/commands/pip_install.rs index b2389d8e0..63ff8af1e 100644 --- a/crates/uv/src/commands/pip_install.rs +++ b/crates/uv/src/commands/pip_install.rs @@ -94,7 +94,6 @@ pub(crate) async fn pip_install( // Read all requirements from the provided sources. let RequirementsSpecification { project, - entries, requirements, constraints, overrides, @@ -161,6 +160,7 @@ pub(crate) async fn pip_install( if reinstall.is_none() && upgrade.is_none() && source_trees.is_empty() + && overrides.is_empty() && site_packages.satisfies(&requirements, &editables, &constraints)? { let num_requirements = requirements.len() + editables.len(); @@ -189,9 +189,10 @@ pub(crate) async fn pip_install( // Collect the set of required hashes. let hasher = if require_hashes { HashStrategy::from_requirements( - entries - .into_iter() - .map(|entry| (entry.requirement, entry.hashes)), + requirements + .iter() + .chain(overrides.iter()) + .map(|entry| (&entry.requirement, entry.hashes.as_slice())), markers, )? } else { @@ -293,6 +294,13 @@ pub(crate) async fn pip_install( requirements }; + // Resolve the overrides from the provided sources. + let overrides = + NamedRequirementsResolver::new(overrides, &hasher, &resolve_dispatch, &client, &index) + .with_reporter(ResolverReporter::from(printer)) + .resolve() + .await?; + // Build all editable distributions. The editables are shared between resolution and // installation, and should live for the duration of the command. If an editable is already // installed in the environment, we'll still re-build it here. diff --git a/crates/uv/src/commands/pip_sync.rs b/crates/uv/src/commands/pip_sync.rs index b2295c1e7..218ad29c3 100644 --- a/crates/uv/src/commands/pip_sync.rs +++ b/crates/uv/src/commands/pip_sync.rs @@ -73,7 +73,6 @@ pub(crate) async fn pip_sync( // Read all requirements from the provided sources. let RequirementsSpecification { project: _, - entries, requirements, constraints: _, overrides: _, @@ -138,9 +137,9 @@ pub(crate) async fn pip_sync( // Collect the set of required hashes. let hasher = if require_hashes { HashStrategy::from_requirements( - entries - .into_iter() - .map(|entry| (entry.requirement, entry.hashes)), + requirements + .iter() + .map(|entry| (&entry.requirement, entry.hashes.as_slice())), markers, )? } else { diff --git a/crates/uv/src/commands/pip_uninstall.rs b/crates/uv/src/commands/pip_uninstall.rs index e4d91154d..45e19da59 100644 --- a/crates/uv/src/commands/pip_uninstall.rs +++ b/crates/uv/src/commands/pip_uninstall.rs @@ -82,7 +82,7 @@ pub(crate) async fn pip_uninstall( let (named, unnamed): (Vec, Vec) = spec .requirements .into_iter() - .partition_map(|requirement| match requirement { + .partition_map(|entry| match entry.requirement { RequirementsTxtRequirement::Pep508(requirement) => Either::Left(requirement), RequirementsTxtRequirement::Unnamed(requirement) => Either::Right(requirement), }); diff --git a/crates/uv/tests/pip_compile.rs b/crates/uv/tests/pip_compile.rs index f196db2a4..22a04aeb6 100644 --- a/crates/uv/tests/pip_compile.rs +++ b/crates/uv/tests/pip_compile.rs @@ -2717,6 +2717,90 @@ fn override_multi_dependency() -> Result<()> { Ok(()) } +/// Flask==3.0.0 depends on Werkzeug>=3.0.0. Demonstrate that we can override this +/// requirement with a URL. +#[test] +fn override_dependency_url() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("flask==3.0.0")?; + + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("werkzeug @ https://files.pythonhosted.org/packages/cc/94/5f7079a0e00bd6863ef8f1da638721e9da21e5bacee597595b318f71d62e/Werkzeug-1.0.1-py2.py3-none-any.whl")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--override") + .arg("overrides.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --override overrides.txt + blinker==1.7.0 + # via flask + click==8.1.7 + # via flask + flask==3.0.0 + itsdangerous==2.1.2 + # via flask + jinja2==3.1.3 + # via flask + markupsafe==2.1.5 + # via jinja2 + werkzeug @ https://files.pythonhosted.org/packages/cc/94/5f7079a0e00bd6863ef8f1da638721e9da21e5bacee597595b318f71d62e/Werkzeug-1.0.1-py2.py3-none-any.whl + # via flask + + ----- stderr ----- + Resolved 7 packages in [TIME] + "### + ); + + Ok(()) +} + +/// Flask==3.0.0 depends on Werkzeug>=3.0.0. Demonstrate that we can override this +/// requirement with an unnamed URL. +#[test] +fn override_dependency_unnamed_url() -> Result<()> { + let context = TestContext::new("3.12"); + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("flask==3.0.0")?; + + let overrides_txt = context.temp_dir.child("overrides.txt"); + overrides_txt.write_str("https://files.pythonhosted.org/packages/cc/94/5f7079a0e00bd6863ef8f1da638721e9da21e5bacee597595b318f71d62e/Werkzeug-1.0.1-py2.py3-none-any.whl")?; + + uv_snapshot!(context.compile() + .arg("requirements.in") + .arg("--override") + .arg("overrides.txt"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + # This file was autogenerated by uv via the following command: + # uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z requirements.in --override overrides.txt + blinker==1.7.0 + # via flask + click==8.1.7 + # via flask + flask==3.0.0 + itsdangerous==2.1.2 + # via flask + jinja2==3.1.3 + # via flask + markupsafe==2.1.5 + # via jinja2 + werkzeug @ https://files.pythonhosted.org/packages/cc/94/5f7079a0e00bd6863ef8f1da638721e9da21e5bacee597595b318f71d62e/Werkzeug-1.0.1-py2.py3-none-any.whl + # via flask + + ----- stderr ----- + Resolved 7 packages in [TIME] + "### + ); + + Ok(()) +} + /// Request an extra that doesn't exist on the specified package. #[test] fn missing_registry_extra() -> Result<()> { From 8507ba872f34c85a459275459a193cb7e75aa7d8 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Thu, 11 Apr 2024 17:23:37 -0400 Subject: [PATCH 110/110] Remove unnecessary hashing from IDs (#2998) ## Summary In all of these ID types, we pass values to `cache_key::digest` prior to passing to `DistributionId` or `ResourceId`. The `DistributionId` and `ResourceId` are then hashed later, since they're used as keys in hash maps. It seems wasteful to hash the value, then hash the hashed value? So this PR modifies those structs to be enums that can take one of a fixed set of types. --- crates/distribution-types/src/id.rs | 42 +++++++++--------- crates/distribution-types/src/lib.rs | 64 ++++++---------------------- 2 files changed, 33 insertions(+), 73 deletions(-) diff --git a/crates/distribution-types/src/id.rs b/crates/distribution-types/src/id.rs index cca3237da..989e566e7 100644 --- a/crates/distribution-types/src/id.rs +++ b/crates/distribution-types/src/id.rs @@ -1,9 +1,11 @@ use std::fmt::{Display, Formatter}; +use std::path::PathBuf; -use cache_key::CanonicalUrl; +use cache_key::{CanonicalUrl, RepositoryUrl}; use url::Url; use pep440_rs::Version; +use pypi_types::HashDigest; use uv_normalize::PackageName; /// A unique identifier for a package. A package can either be identified by a name (e.g., `black`) @@ -13,7 +15,7 @@ pub enum PackageId { /// The identifier consists of a package name. Name(PackageName), /// The identifier consists of a URL. - Url(String), + Url(CanonicalUrl), } impl PackageId { @@ -24,7 +26,7 @@ impl PackageId { /// Create a new [`PackageId`] from a URL. pub fn from_url(url: &Url) -> Self { - Self::Url(cache_key::digest(&CanonicalUrl::new(url))) + Self::Url(CanonicalUrl::new(url)) } } @@ -43,7 +45,7 @@ pub enum VersionId { /// The identifier consists of a package name and version. NameVersion(PackageName, Version), /// The identifier consists of a URL. - Url(String), + Url(CanonicalUrl), } impl VersionId { @@ -54,7 +56,7 @@ impl VersionId { /// Create a new [`VersionId`] from a URL. pub fn from_url(url: &Url) -> Self { - Self::Url(cache_key::digest(&CanonicalUrl::new(url))) + Self::Url(CanonicalUrl::new(url)) } } @@ -81,28 +83,22 @@ impl Display for VersionId { /// that the ID is unique within a single invocation of the resolver (and so, e.g., a hash of /// the URL would also be sufficient). #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct DistributionId(String); - -impl DistributionId { - pub fn new(id: impl Into) -> Self { - Self(id.into()) - } -} - -impl DistributionId { - pub fn as_str(&self) -> &str { - &self.0 - } +pub enum DistributionId { + Url(CanonicalUrl), + PathBuf(PathBuf), + Digest(HashDigest), + AbsoluteUrl(String), + RelativeUrl(String, String), } /// A unique identifier for a resource, like a URL or a Git repository. #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ResourceId(String); - -impl ResourceId { - pub fn new(id: impl Into) -> Self { - Self(id.into()) - } +pub enum ResourceId { + Url(RepositoryUrl), + PathBuf(PathBuf), + Digest(HashDigest), + AbsoluteUrl(String), + RelativeUrl(String, String), } impl From<&Self> for VersionId { diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index f4ebe5c3c..600745586 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -791,18 +791,18 @@ impl RemoteSource for Dist { impl Identifier for Url { fn distribution_id(&self) -> DistributionId { - DistributionId::new(cache_key::digest(&cache_key::CanonicalUrl::new(self))) + DistributionId::Url(cache_key::CanonicalUrl::new(self)) } fn resource_id(&self) -> ResourceId { - ResourceId::new(cache_key::digest(&cache_key::RepositoryUrl::new(self))) + ResourceId::Url(cache_key::RepositoryUrl::new(self)) } } impl Identifier for File { fn distribution_id(&self) -> DistributionId { if let Some(hash) = self.hashes.first() { - DistributionId::new(&*hash.digest) + DistributionId::Digest(hash.clone()) } else { self.url.distribution_id() } @@ -810,7 +810,7 @@ impl Identifier for File { fn resource_id(&self) -> ResourceId { if let Some(hash) = self.hashes.first() { - ResourceId::new(&*hash.digest) + ResourceId::Digest(hash.clone()) } else { self.url.resource_id() } @@ -819,67 +819,31 @@ impl Identifier for File { impl Identifier for Path { fn distribution_id(&self) -> DistributionId { - DistributionId::new(cache_key::digest(&self)) + DistributionId::PathBuf(self.to_path_buf()) } fn resource_id(&self) -> ResourceId { - ResourceId::new(cache_key::digest(&self)) - } -} - -impl Identifier for String { - fn distribution_id(&self) -> DistributionId { - DistributionId::new(cache_key::digest(&self)) - } - - fn resource_id(&self) -> ResourceId { - ResourceId::new(cache_key::digest(&self)) - } -} - -impl Identifier for &str { - fn distribution_id(&self) -> DistributionId { - DistributionId::new(cache_key::digest(&self)) - } - - fn resource_id(&self) -> ResourceId { - ResourceId::new(cache_key::digest(&self)) - } -} - -impl Identifier for (&str, &str) { - fn distribution_id(&self) -> DistributionId { - DistributionId::new(cache_key::digest(&self)) - } - - fn resource_id(&self) -> ResourceId { - ResourceId::new(cache_key::digest(&self)) - } -} - -impl Identifier for (&Url, &str) { - fn distribution_id(&self) -> DistributionId { - DistributionId::new(cache_key::digest(&self)) - } - - fn resource_id(&self) -> ResourceId { - ResourceId::new(cache_key::digest(&self)) + ResourceId::PathBuf(self.to_path_buf()) } } impl Identifier for FileLocation { fn distribution_id(&self) -> DistributionId { match self { - Self::RelativeUrl(base, url) => (base.as_str(), url.as_str()).distribution_id(), - Self::AbsoluteUrl(url) => url.distribution_id(), + Self::RelativeUrl(base, url) => { + DistributionId::RelativeUrl(base.to_string(), url.to_string()) + } + Self::AbsoluteUrl(url) => DistributionId::AbsoluteUrl(url.to_string()), Self::Path(path) => path.distribution_id(), } } fn resource_id(&self) -> ResourceId { match self { - Self::RelativeUrl(base, url) => (base.as_str(), url.as_str()).resource_id(), - Self::AbsoluteUrl(url) => url.resource_id(), + Self::RelativeUrl(base, url) => { + ResourceId::RelativeUrl(base.to_string(), url.to_string()) + } + Self::AbsoluteUrl(url) => ResourceId::AbsoluteUrl(url.to_string()), Self::Path(path) => path.resource_id(), } }