Avoid generating unused hashes during `uv lock` (#10307)

## Summary

We don't even use these! See the comment inline.

Closes https://github.com/astral-sh/uv/issues/9651.
This commit is contained in:
Charlie Marsh 2025-01-05 19:58:07 -05:00 committed by GitHub
parent bbf9558b16
commit 7182a34aa4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 64 additions and 39 deletions

View File

@ -5,7 +5,7 @@ pub enum HashPolicy<'a> {
/// No hash policy is specified. /// No hash policy is specified.
None, None,
/// Hashes should be generated (specifically, a SHA-256 hash), but not validated. /// Hashes should be generated (specifically, a SHA-256 hash), but not validated.
Generate, Generate(HashGeneration),
/// Hashes should be validated against a pre-defined list of hashes. If necessary, hashes should /// 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. /// be generated so as to ensure that the archive is valid.
Validate(&'a [HashDigest]), Validate(&'a [HashDigest]),
@ -17,21 +17,28 @@ impl HashPolicy<'_> {
matches!(self, Self::None) 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`. /// Returns `true` if the hash policy is `Validate`.
pub fn is_validate(&self) -> bool { pub fn is_validate(&self) -> bool {
matches!(self, Self::Validate(_)) matches!(self, Self::Validate(_))
} }
/// Returns `true` if the hash policy indicates that hashes should be generated.
pub fn is_generate(&self, dist: &crate::BuiltDist) -> bool {
match self {
HashPolicy::Generate(HashGeneration::Url) => dist.file().is_none(),
HashPolicy::Generate(HashGeneration::All) => {
dist.file().map_or(true, |file| file.hashes.is_empty())
}
HashPolicy::Validate(_) => false,
HashPolicy::None => false,
}
}
/// Return the algorithms used in the hash policy. /// Return the algorithms used in the hash policy.
pub fn algorithms(&self) -> Vec<HashAlgorithm> { pub fn algorithms(&self) -> Vec<HashAlgorithm> {
match self { match self {
Self::None => vec![], Self::None => vec![],
Self::Generate => vec![HashAlgorithm::Sha256], Self::Generate(_) => vec![HashAlgorithm::Sha256],
Self::Validate(hashes) => { Self::Validate(hashes) => {
let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::<Vec<_>>(); let mut algorithms = hashes.iter().map(HashDigest::algorithm).collect::<Vec<_>>();
algorithms.sort(); algorithms.sort();
@ -45,12 +52,22 @@ impl HashPolicy<'_> {
pub fn digests(&self) -> &[HashDigest] { pub fn digests(&self) -> &[HashDigest] {
match self { match self {
Self::None => &[], Self::None => &[],
Self::Generate => &[], Self::Generate(_) => &[],
Self::Validate(hashes) => hashes, Self::Validate(hashes) => hashes,
} }
} }
} }
/// The context in which hashes should be generated.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum HashGeneration {
/// Generate hashes for direct URL distributions.
Url,
/// Generate hashes for direct URL distributions, along with any distributions that are hosted
/// on a registry that does _not_ provide hashes.
All,
}
pub trait Hashed { pub trait Hashed {
/// Return the [`HashDigest`]s for the archive. /// Return the [`HashDigest`]s for the archive.
fn hashes(&self) -> &[HashDigest]; fn hashes(&self) -> &[HashDigest];
@ -59,7 +76,7 @@ pub trait Hashed {
fn satisfies(&self, hashes: HashPolicy) -> bool { fn satisfies(&self, hashes: HashPolicy) -> bool {
match hashes { match hashes {
HashPolicy::None => true, HashPolicy::None => true,
HashPolicy::Generate => self HashPolicy::Generate(_) => self
.hashes() .hashes()
.iter() .iter()
.any(|hash| hash.algorithm == HashAlgorithm::Sha256), .any(|hash| hash.algorithm == HashAlgorithm::Sha256),
@ -71,7 +88,7 @@ pub trait Hashed {
fn has_digests(&self, hashes: HashPolicy) -> bool { fn has_digests(&self, hashes: HashPolicy) -> bool {
match hashes { match hashes {
HashPolicy::None => true, HashPolicy::None => true,
HashPolicy::Generate => self HashPolicy::Generate(_) => self
.hashes() .hashes()
.iter() .iter()
.any(|hash| hash.algorithm == HashAlgorithm::Sha256), .any(|hash| hash.algorithm == HashAlgorithm::Sha256),

View File

@ -405,14 +405,21 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); return Ok(ArchiveMetadata::from_metadata23(metadata.clone()));
} }
// If hash generation is enabled, and the distribution isn't hosted on an index, get the // If hash generation is enabled, and the distribution isn't hosted on a registry, get the
// entire wheel to ensure that the hashes are included in the response. If the distribution // 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. // 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 // For hash _validation_, callers are expected to enforce the policy when retrieving the
// wheel. // wheel.
//
// Historically, for `uv pip compile --universal`, we also generate hashes for
// registry-based distributions when the relevant registry doesn't provide them. This was
// motivated by `--find-links`. We continue that behavior (under `HashGeneration::All`) for
// backwards compatibility, but it's a little dubious, since we're only hashing _one_
// distribution here (as opposed to hashing all distributions for the version), and it may
// not even be a compatible distribution!
//
// TODO(charlie): Request the hashes via a separate method, to reduce the coupling in this API. // TODO(charlie): Request the hashes via a separate method, to reduce the coupling in this API.
if hashes.is_generate() { if hashes.is_generate(dist) {
if dist.file().map_or(true, |file| file.hashes.is_empty()) {
let wheel = self.get_wheel(dist, hashes).await?; let wheel = self.get_wheel(dist, hashes).await?;
let metadata = wheel.metadata()?; let metadata = wheel.metadata()?;
let hashes = wheel.hashes; let hashes = wheel.hashes;
@ -421,7 +428,6 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> {
hashes, hashes,
}); });
} }
}
let result = self let result = self
.client .client

View File

@ -13,7 +13,7 @@ use url::Url;
use uv_configuration::ExtrasSpecification; use uv_configuration::ExtrasSpecification;
use uv_distribution::{DistributionDatabase, Reporter, RequiresDist}; use uv_distribution::{DistributionDatabase, Reporter, RequiresDist};
use uv_distribution_types::{ use uv_distribution_types::{
BuildableSource, DirectorySourceUrl, HashPolicy, SourceUrl, VersionId, BuildableSource, DirectorySourceUrl, HashGeneration, HashPolicy, SourceUrl, VersionId,
}; };
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_normalize::{ExtraName, PackageName}; use uv_normalize::{ExtraName, PackageName};
@ -213,8 +213,8 @@ impl<'a, Context: BuildContext> SourceTreeResolver<'a, Context> {
// manual match. // manual match.
let hashes = match self.hasher { let hashes = match self.hasher {
HashStrategy::None => HashPolicy::None, HashStrategy::None => HashPolicy::None,
HashStrategy::Generate => HashPolicy::Generate, HashStrategy::Generate(mode) => HashPolicy::Generate(*mode),
HashStrategy::Verify(_) => HashPolicy::Generate, HashStrategy::Verify(_) => HashPolicy::Generate(HashGeneration::All),
HashStrategy::Require(_) => { HashStrategy::Require(_) => {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"Hash-checking is not supported for local directories: {}", "Hash-checking is not supported for local directories: {}",

View File

@ -5,7 +5,8 @@ use url::Url;
use uv_configuration::HashCheckingMode; use uv_configuration::HashCheckingMode;
use uv_distribution_types::{ use uv_distribution_types::{
DistributionMetadata, HashPolicy, Name, Resolution, UnresolvedRequirement, VersionId, DistributionMetadata, HashGeneration, HashPolicy, Name, Resolution, UnresolvedRequirement,
VersionId,
}; };
use uv_normalize::PackageName; use uv_normalize::PackageName;
use uv_pep440::Version; use uv_pep440::Version;
@ -19,7 +20,7 @@ pub enum HashStrategy {
#[default] #[default]
None, None,
/// Hashes should be generated (specifically, a SHA-256 hash), but not validated. /// Hashes should be generated (specifically, a SHA-256 hash), but not validated.
Generate, Generate(HashGeneration),
/// Hashes should be validated, if present, but ignored if absent. /// Hashes should be validated, if present, but ignored if absent.
/// ///
/// If necessary, hashes should be generated to ensure that the archive is valid. /// If necessary, hashes should be generated to ensure that the archive is valid.
@ -35,7 +36,7 @@ impl HashStrategy {
pub fn get<T: DistributionMetadata>(&self, distribution: &T) -> HashPolicy { pub fn get<T: DistributionMetadata>(&self, distribution: &T) -> HashPolicy {
match self { match self {
Self::None => HashPolicy::None, Self::None => HashPolicy::None,
Self::Generate => HashPolicy::Generate, Self::Generate(mode) => HashPolicy::Generate(*mode),
Self::Verify(hashes) => { Self::Verify(hashes) => {
if let Some(hashes) = hashes.get(&distribution.version_id()) { if let Some(hashes) = hashes.get(&distribution.version_id()) {
HashPolicy::Validate(hashes.as_slice()) HashPolicy::Validate(hashes.as_slice())
@ -56,7 +57,7 @@ impl HashStrategy {
pub fn get_package(&self, name: &PackageName, version: &Version) -> HashPolicy { pub fn get_package(&self, name: &PackageName, version: &Version) -> HashPolicy {
match self { match self {
Self::None => HashPolicy::None, Self::None => HashPolicy::None,
Self::Generate => HashPolicy::Generate, Self::Generate(mode) => HashPolicy::Generate(*mode),
Self::Verify(hashes) => { Self::Verify(hashes) => {
if let Some(hashes) = if let Some(hashes) =
hashes.get(&VersionId::from_registry(name.clone(), version.clone())) hashes.get(&VersionId::from_registry(name.clone(), version.clone()))
@ -79,7 +80,7 @@ impl HashStrategy {
pub fn get_url(&self, url: &Url) -> HashPolicy { pub fn get_url(&self, url: &Url) -> HashPolicy {
match self { match self {
Self::None => HashPolicy::None, Self::None => HashPolicy::None,
Self::Generate => HashPolicy::Generate, Self::Generate(mode) => HashPolicy::Generate(*mode),
Self::Verify(hashes) => { Self::Verify(hashes) => {
if let Some(hashes) = hashes.get(&VersionId::from_url(url)) { if let Some(hashes) = hashes.get(&VersionId::from_url(url)) {
HashPolicy::Validate(hashes.as_slice()) HashPolicy::Validate(hashes.as_slice())
@ -100,7 +101,7 @@ impl HashStrategy {
pub fn allows_package(&self, name: &PackageName, version: &Version) -> bool { pub fn allows_package(&self, name: &PackageName, version: &Version) -> bool {
match self { match self {
Self::None => true, Self::None => true,
Self::Generate => true, Self::Generate(_) => true,
Self::Verify(_) => true, Self::Verify(_) => true,
Self::Require(hashes) => { Self::Require(hashes) => {
hashes.contains_key(&VersionId::from_registry(name.clone(), version.clone())) hashes.contains_key(&VersionId::from_registry(name.clone(), version.clone()))
@ -112,7 +113,7 @@ impl HashStrategy {
pub fn allows_url(&self, url: &Url) -> bool { pub fn allows_url(&self, url: &Url) -> bool {
match self { match self {
Self::None => true, Self::None => true,
Self::Generate => true, Self::Generate(_) => true,
Self::Verify(_) => true, Self::Verify(_) => true,
Self::Require(hashes) => hashes.contains_key(&VersionId::from_url(url)), Self::Require(hashes) => hashes.contains_key(&VersionId::from_url(url)),
} }

View File

@ -17,8 +17,8 @@ use uv_configuration::{
use uv_configuration::{KeyringProviderType, TargetTriple}; use uv_configuration::{KeyringProviderType, TargetTriple};
use uv_dispatch::{BuildDispatch, SharedState}; use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution_types::{ use uv_distribution_types::{
DependencyMetadata, Index, IndexLocations, NameRequirementSpecification, Origin, DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification,
UnresolvedRequirementSpecification, Verbatim, Origin, UnresolvedRequirementSpecification, Verbatim,
}; };
use uv_fs::Simplified; use uv_fs::Simplified;
use uv_install_wheel::linker::LinkMode; use uv_install_wheel::linker::LinkMode;
@ -266,7 +266,7 @@ pub(crate) async fn pip_compile(
// Generate, but don't enforce hashes for the requirements. // Generate, but don't enforce hashes for the requirements.
let hasher = if generate_hashes { let hasher = if generate_hashes {
HashStrategy::Generate HashStrategy::Generate(HashGeneration::All)
} else { } else {
HashStrategy::None HashStrategy::None
}; };

View File

@ -17,7 +17,7 @@ use uv_configuration::{
use uv_dispatch::{BuildDispatch, SharedState}; use uv_dispatch::{BuildDispatch, SharedState};
use uv_distribution::DistributionDatabase; use uv_distribution::DistributionDatabase;
use uv_distribution_types::{ use uv_distribution_types::{
DependencyMetadata, Index, IndexLocations, NameRequirementSpecification, DependencyMetadata, HashGeneration, Index, IndexLocations, NameRequirementSpecification,
UnresolvedRequirementSpecification, UnresolvedRequirementSpecification,
}; };
use uv_git::ResolvedRepositoryReference; use uv_git::ResolvedRepositoryReference;
@ -472,7 +472,7 @@ async fn do_lock(
.index_strategy(index_strategy) .index_strategy(index_strategy)
.build_options(build_options.clone()) .build_options(build_options.clone())
.build(); .build();
let hasher = HashStrategy::Generate; let hasher = HashStrategy::Generate(HashGeneration::Url);
// TODO(charlie): These are all default values. We should consider whether we want to make them // TODO(charlie): These are all default values. We should consider whether we want to make them
// optional on the downstream APIs. // optional on the downstream APIs.

View File

@ -9307,7 +9307,7 @@ fn lock_find_links_local_wheel() -> Result<()> {
----- stderr ----- ----- stderr -----
Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] Using CPython 3.12.[X] interpreter at: [PYTHON-3.12]
Creating virtual environment at: .venv Creating virtual environment at: .venv
Prepared 1 package in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/workspace) + project==0.1.0 (from file://[TEMP_DIR]/workspace)
+ tqdm==1000.0.0 + tqdm==1000.0.0
@ -9518,7 +9518,7 @@ fn lock_find_links_http_wheel() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Prepared 1 package in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ packaging==23.2 + packaging==23.2
+ project==0.1.0 (from file://[TEMP_DIR]/) + project==0.1.0 (from file://[TEMP_DIR]/)
@ -9744,7 +9744,7 @@ fn lock_local_index() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Prepared 1 package in [TIME] Prepared 2 packages in [TIME]
Installed 2 packages in [TIME] Installed 2 packages in [TIME]
+ project==0.1.0 (from file://[TEMP_DIR]/) + project==0.1.0 (from file://[TEMP_DIR]/)
+ tqdm==1000.0.0 + tqdm==1000.0.0

View File

@ -5870,6 +5870,7 @@ fn sync_build_tag() -> Result<()> {
----- stdout ----- ----- stdout -----
----- stderr ----- ----- stderr -----
Prepared 1 package in [TIME]
Installed 1 package in [TIME] Installed 1 package in [TIME]
+ build-tag==1.0.0 + build-tag==1.0.0
"###); "###);