From fda227616ca598dd687fa4d684035cfb3ca82c13 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 17 Sep 2024 23:18:05 -0400 Subject: [PATCH] Allow users to provide pre-defined metadata for resolution (#7442) ## Summary This PR enables users to provide pre-defined static metadata for dependencies. It's intended for situations in which the user depends on a package that does _not_ declare static metadata (e.g., a `setup.py`-only sdist), and that is expensive to build or even cannot be built on some architectures. For example, you might have a Linux-only dependency that can't be built on ARM -- but we need to build that package in order to generate the lockfile. By providing static metadata, the user can instruct uv to avoid building that package at all. For example, to override all `anyio` versions: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.12" dependencies = ["anyio"] [[tool.uv.dependency-metadata]] name = "anyio" requires-dist = ["iniconfig"] ``` Or, to override a specific version: ```toml [project] name = "project" version = "0.1.0" requires-python = ">=3.12" dependencies = ["anyio"] [[tool.uv.dependency-metadata]] name = "anyio" version = "3.7.0" requires-dist = ["iniconfig"] ``` The current implementation uses `Metadata23` directly, so we adhere to the exact schema expected internally and defined by the standards. Any entries are treated similarly to overrides, in that we won't even look for `anyio@3.7.0` metadata in the above example. (In a way, this also enables #4422, since you could remove a dependency for a specific package, though it's probably too unwieldy to use in practice, since you'd need to redefine the _rest_ of the metadata, and do that for every package that requires the package you want to omit.) This is under-documented, since I want to get feedback on the core ideas and names involved. Closes https://github.com/astral-sh/uv/issues/7393. --- crates/bench/benches/uv.rs | 14 +- .../src/dependency_metadata.rs | 76 ++++++ crates/distribution-types/src/lib.rs | 2 + crates/pep508-rs/src/lib.rs | 8 +- crates/uv-cli/src/options.rs | 2 + crates/uv-dispatch/src/lib.rs | 10 +- .../src/distribution_database.rs | 22 ++ crates/uv-resolver/src/lock/mod.rs | 63 ++++- ...r__lock__tests__hash_optional_missing.snap | 1 + ...r__lock__tests__hash_optional_present.snap | 1 + ...r__lock__tests__hash_required_present.snap | 1 + ...missing_dependency_source_unambiguous.snap | 1 + ...dependency_source_version_unambiguous.snap | 1 + ...issing_dependency_version_unambiguous.snap | 1 + ...lock__tests__source_direct_has_subdir.snap | 1 + ..._lock__tests__source_direct_no_subdir.snap | 1 + ...solver__lock__tests__source_directory.snap | 1 + ...esolver__lock__tests__source_editable.snap | 1 + crates/uv-settings/src/settings.rs | 53 ++++- crates/uv-types/src/traits.rs | 6 +- crates/uv/src/commands/build.rs | 2 + crates/uv/src/commands/pip/compile.rs | 4 +- crates/uv/src/commands/pip/install.rs | 5 +- crates/uv/src/commands/pip/sync.rs | 4 +- crates/uv/src/commands/project/add.rs | 1 + crates/uv/src/commands/project/lock.rs | 25 +- crates/uv/src/commands/project/mod.rs | 8 + crates/uv/src/commands/project/sync.rs | 2 + crates/uv/src/commands/venv.rs | 6 +- crates/uv/src/lib.rs | 9 +- crates/uv/src/settings.rs | 29 ++- crates/uv/tests/lock.rs | 218 ++++++++++++++++++ crates/uv/tests/show_settings.rs | 70 ++++++ docs/concepts/projects.md | 60 ++++- docs/concepts/resolution.md | 95 ++++++-- docs/reference/settings.md | 83 +++++++ uv.schema.json | 60 ++++- 37 files changed, 897 insertions(+), 50 deletions(-) create mode 100644 crates/distribution-types/src/dependency_metadata.rs diff --git a/crates/bench/benches/uv.rs b/crates/bench/benches/uv.rs index 0e839a8d6..e5f7a9a69 100644 --- a/crates/bench/benches/uv.rs +++ b/crates/bench/benches/uv.rs @@ -83,7 +83,7 @@ mod resolver { use anyhow::Result; - use distribution_types::{IndexCapabilities, IndexLocations}; + use distribution_types::{DependencyMetadata, IndexCapabilities, IndexLocations}; use install_wheel_rs::linker::LinkMode; use pep440_rs::Version; use pep508_rs::{MarkerEnvironment, MarkerEnvironmentBuilder}; @@ -139,7 +139,7 @@ mod resolver { interpreter: &Interpreter, universal: bool, ) -> Result { - let build_isolation = BuildIsolation::Isolated; + let build_isolation = BuildIsolation::default(); let build_options = BuildOptions::default(); let concurrency = Concurrency::default(); let config_settings = ConfigSettings::default(); @@ -150,17 +150,18 @@ mod resolver { .timestamp() .into(), ); + let build_constraints = Constraints::default(); + let capabilities = IndexCapabilities::default(); let flat_index = FlatIndex::default(); let git = GitResolver::default(); - let capabilities = IndexCapabilities::default(); - let hashes = HashStrategy::None; + let hashes = HashStrategy::default(); let in_flight = InFlight::default(); let index = InMemoryIndex::default(); let index_locations = IndexLocations::default(); let installed_packages = EmptyInstalledPackages; - let sources = SourceStrategy::default(); let options = OptionsBuilder::new().exclude_newer(exclude_newer).build(); - let build_constraints = Constraints::default(); + let sources = SourceStrategy::default(); + let dependency_metadata = DependencyMetadata::default(); let python_requirement = if universal { PythonRequirement::from_requires_python( @@ -178,6 +179,7 @@ mod resolver { interpreter, &index_locations, &flat_index, + &dependency_metadata, &index, &git, &capabilities, diff --git a/crates/distribution-types/src/dependency_metadata.rs b/crates/distribution-types/src/dependency_metadata.rs new file mode 100644 index 000000000..4512f5cb6 --- /dev/null +++ b/crates/distribution-types/src/dependency_metadata.rs @@ -0,0 +1,76 @@ +use pep440_rs::{Version, VersionSpecifiers}; +use pep508_rs::Requirement; +use pypi_types::{Metadata23, VerbatimParsedUrl}; +use rustc_hash::FxHashMap; +use serde::{Deserialize, Serialize}; +use uv_normalize::{ExtraName, PackageName}; + +/// Pre-defined [`StaticMetadata`] entries, indexed by [`PackageName`] and [`Version`]. +#[derive(Debug, Clone, Default)] +pub struct DependencyMetadata(FxHashMap>); + +impl DependencyMetadata { + /// Index a set of [`StaticMetadata`] entries by [`PackageName`] and [`Version`]. + pub fn from_entries(entries: impl IntoIterator) -> Self { + let mut map = Self::default(); + for entry in entries { + map.0.entry(entry.name.clone()).or_default().push(entry); + } + map + } + + /// Retrieve a [`StaticMetadata`] entry by [`PackageName`] and [`Version`]. + pub fn get(&self, package: &PackageName, version: &Version) -> Option { + let versions = self.0.get(package)?; + + // Search for an exact, then a global match. + let metadata = versions + .iter() + .find(|v| v.version.as_ref() == Some(version)) + .or_else(|| versions.iter().find(|v| v.version.is_none()))?; + + Some(Metadata23 { + name: metadata.name.clone(), + version: version.clone(), + requires_dist: metadata.requires_dist.clone(), + requires_python: metadata.requires_python.clone(), + provides_extras: metadata.provides_extras.clone(), + }) + } + + /// Retrieve all [`StaticMetadata`] entries. + pub fn values(&self) -> impl Iterator { + self.0.values().flatten() + } +} + +/// A subset of the Python Package Metadata 2.3 standard as specified in +/// . +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] +#[serde(rename_all = "kebab-case")] +pub struct StaticMetadata { + // Mandatory fields + pub name: PackageName, + #[cfg_attr( + feature = "schemars", + schemars( + with = "String", + description = "PEP 440-style package version, e.g., `1.2.3`" + ) + )] + pub version: Option, + // Optional fields + #[serde(default)] + pub requires_dist: Vec>, + #[cfg_attr( + feature = "schemars", + schemars( + with = "Option", + description = "PEP 508-style Python requirement, e.g., `>=3.10`" + ) + )] + pub requires_python: Option, + #[serde(default)] + pub provides_extras: Vec, +} diff --git a/crates/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index c1771ef34..7c48fb38e 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/distribution-types/src/lib.rs @@ -51,6 +51,7 @@ pub use crate::annotation::*; pub use crate::any::*; pub use crate::buildable::*; pub use crate::cached::*; +pub use crate::dependency_metadata::*; pub use crate::diagnostic::*; pub use crate::error::*; pub use crate::file::*; @@ -68,6 +69,7 @@ mod annotation; mod any; mod buildable; mod cached; +mod dependency_metadata; mod diagnostic; mod error; mod file; diff --git a/crates/pep508-rs/src/lib.rs b/crates/pep508-rs/src/lib.rs index 0619d9e2a..01d1e40b5 100644 --- a/crates/pep508-rs/src/lib.rs +++ b/crates/pep508-rs/src/lib.rs @@ -135,7 +135,7 @@ create_exception!( ); /// A PEP 508 dependency specifier. -#[derive(Hash, Debug, Clone, Eq, PartialEq)] +#[derive(Hash, Debug, Clone, Eq, PartialEq, Ord, PartialOrd)] pub struct Requirement { /// The distribution name such as `requests` in /// `requests [security,tests] >= 2.8.1, == 2.8.* ; python_version > "3.8"`. @@ -480,7 +480,9 @@ impl schemars::JsonSchema for Requirement { schemars::schema::SchemaObject { instance_type: Some(schemars::schema::InstanceType::String.into()), metadata: Some(Box::new(schemars::schema::Metadata { - description: Some("A PEP 508 dependency specifier".to_string()), + description: Some( + "A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`".to_string(), + ), ..schemars::schema::Metadata::default() })), ..schemars::schema::SchemaObject::default() @@ -535,7 +537,7 @@ impl Extras { } /// The actual version specifier or URL to install. -#[derive(Debug, Clone, Eq, Hash, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq, Ord, PartialOrd, Hash)] pub enum VersionOrUrl { /// A PEP 440 version specifier set VersionSpecifier(VersionSpecifiers), diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs index dc6621712..fb7cd5235 100644 --- a/crates/uv-cli/src/options.rs +++ b/crates/uv-cli/src/options.rs @@ -271,6 +271,7 @@ pub fn resolver_options( } else { prerelease }, + dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), no_build_isolation: flag(no_build_isolation, build_isolation), @@ -364,6 +365,7 @@ pub fn resolver_installer_options( } else { prerelease }, + dependency_metadata: None, config_settings: config_setting .map(|config_settings| config_settings.into_iter().collect::()), no_build_isolation: flag(no_build_isolation, build_isolation), diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index 00fc00be6..7eadb584c 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -12,7 +12,8 @@ use rustc_hash::FxHashMap; use tracing::{debug, instrument}; use distribution_types::{ - CachedDist, IndexCapabilities, IndexLocations, Name, Resolution, SourceDist, VersionOrUrlRef, + CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, Name, Resolution, + SourceDist, VersionOrUrlRef, }; use pypi_types::Requirement; use uv_build::{SourceBuild, SourceBuildContext}; @@ -45,6 +46,7 @@ pub struct BuildDispatch<'a> { index: &'a InMemoryIndex, git: &'a GitResolver, capabilities: &'a IndexCapabilities, + dependency_metadata: &'a DependencyMetadata, in_flight: &'a InFlight, build_isolation: BuildIsolation<'a>, link_mode: install_wheel_rs::linker::LinkMode, @@ -66,6 +68,7 @@ impl<'a> BuildDispatch<'a> { interpreter: &'a Interpreter, index_locations: &'a IndexLocations, flat_index: &'a FlatIndex, + dependency_metadata: &'a DependencyMetadata, index: &'a InMemoryIndex, git: &'a GitResolver, capabilities: &'a IndexCapabilities, @@ -90,6 +93,7 @@ impl<'a> BuildDispatch<'a> { index, git, capabilities, + dependency_metadata, in_flight, index_strategy, config_settings, @@ -136,6 +140,10 @@ impl<'a> BuildContext for BuildDispatch<'a> { self.capabilities } + fn dependency_metadata(&self) -> &DependencyMetadata { + self.dependency_metadata + } + fn build_options(&self) -> &BuildOptions { self.build_options } diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index 0a67c6972..55c3871ba 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -360,6 +360,15 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { dist: &BuiltDist, hashes: HashPolicy<'_>, ) -> Result { + // If the metadata was provided by the user directly, prefer it. + if let Some(metadata) = self + .build_context + .dependency_metadata() + .get(dist.name(), dist.version()) + { + return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); + } + // 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. @@ -415,6 +424,19 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { source: &BuildableSource<'_>, hashes: HashPolicy<'_>, ) -> Result { + // If the metadata was provided by the user directly, prefer it. + if let Some(dist) = source.as_dist() { + if let Some(version) = dist.version() { + if let Some(metadata) = self + .build_context + .dependency_metadata() + .get(dist.name(), version) + { + return Ok(ArchiveMetadata::from_metadata23(metadata.clone())); + } + } + } + // Optimization: Skip source dist download when we must not build them anyway. if self .build_context diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index 0bf90bd30..28fdc0148 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -17,11 +17,11 @@ use url::Url; use cache_key::RepositoryUrl; use distribution_filename::{DistExtension, ExtensionError, SourceDistExtension, WheelFilename}; use distribution_types::{ - BuiltDist, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, Dist, - DistributionMetadata, FileLocation, FlatIndexLocation, GitSourceDist, HashPolicy, + BuiltDist, DependencyMetadata, DirectUrlBuiltDist, DirectUrlSourceDist, DirectorySourceDist, + Dist, DistributionMetadata, FileLocation, FlatIndexLocation, GitSourceDist, HashPolicy, IndexLocations, IndexUrl, Name, PathBuiltDist, PathSourceDist, RegistryBuiltDist, - RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, ToUrlError, - UrlString, + RegistryBuiltWheel, RegistrySourceDist, RemoteSource, Resolution, ResolvedDist, StaticMetadata, + ToUrlError, UrlString, }; use pep440_rs::Version; use pep508_rs::{split_scheme, MarkerEnvironment, MarkerTree, VerbatimUrl, VerbatimUrlError}; @@ -795,6 +795,40 @@ impl Lock { manifest_table.insert("overrides", value(overrides)); } + if !self.manifest.dependency_metadata.is_empty() { + let mut tables = ArrayOfTables::new(); + for metadata in &self.manifest.dependency_metadata { + let mut table = Table::new(); + table.insert("name", value(metadata.name.to_string())); + if let Some(version) = metadata.version.as_ref() { + table.insert("version", value(version.to_string())); + } + if !metadata.requires_dist.is_empty() { + table.insert( + "requires-dist", + value(serde::Serialize::serialize( + &metadata.requires_dist, + toml_edit::ser::ValueSerializer::new(), + )?), + ); + } + if let Some(requires_python) = metadata.requires_python.as_ref() { + table.insert("requires-python", value(requires_python.to_string())); + } + if !metadata.provides_extras.is_empty() { + table.insert( + "provides-extras", + value(serde::Serialize::serialize( + &metadata.provides_extras, + toml_edit::ser::ValueSerializer::new(), + )?), + ); + } + tables.push(table); + } + manifest_table.insert("dependency-metadata", Item::ArrayOfTables(tables)); + } + if !manifest_table.is_empty() { doc.insert("manifest", Item::Table(manifest_table)); } @@ -881,6 +915,7 @@ impl Lock { requirements: &[Requirement], constraints: &[Requirement], overrides: &[Requirement], + dependency_metadata: &DependencyMetadata, indexes: Option<&IndexLocations>, build_options: &BuildOptions, tags: &Tags, @@ -995,6 +1030,18 @@ impl Lock { } } + // Validate that the lockfile was generated with the same static metadata. + { + let expected = dependency_metadata + .values() + .cloned() + .collect::>(); + let actual = &self.manifest.dependency_metadata; + if expected != *actual { + return Ok(SatisfiesResult::MismatchedStaticMetadata(expected, actual)); + } + } + // Collect the set of available indexes (both `--index-url` and `--find-links` entries). let remotes = indexes.map(|locations| { locations @@ -1249,6 +1296,8 @@ pub enum SatisfiesResult<'lock> { MismatchedConstraints(BTreeSet, BTreeSet), /// The lockfile uses a different set of overrides. MismatchedOverrides(BTreeSet, BTreeSet), + /// The lockfile uses different static metadata. + MismatchedStaticMetadata(BTreeSet, &'lock BTreeSet), /// The lockfile is missing a workspace member. MissingRoot(PackageName), /// The lockfile referenced a remote index that was not provided @@ -1302,6 +1351,9 @@ pub struct ResolverManifest { /// The overrides provided to the resolver. #[serde(default)] overrides: BTreeSet, + /// The static metadata provided to the resolver. + #[serde(default)] + dependency_metadata: BTreeSet, } impl ResolverManifest { @@ -1312,12 +1364,14 @@ impl ResolverManifest { requirements: impl IntoIterator, constraints: impl IntoIterator, overrides: impl IntoIterator, + dependency_metadata: impl IntoIterator, ) -> Self { Self { members: members.into_iter().collect(), requirements: requirements.into_iter().collect(), constraints: constraints.into_iter().collect(), overrides: overrides.into_iter().collect(), + dependency_metadata: dependency_metadata.into_iter().collect(), } } @@ -1340,6 +1394,7 @@ impl ResolverManifest { .into_iter() .map(|requirement| requirement.relative_to(workspace.install_path())) .collect::, _>>()?, + dependency_metadata: self.dependency_metadata, }) } } diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap index b4f8edfa3..9eae35fa0 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_missing.snap @@ -105,6 +105,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap index 6fb234b37..b08ac136e 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_optional_present.snap @@ -112,6 +112,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap index 742ce5519..18a9968fa 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__hash_required_present.snap @@ -104,6 +104,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap index 38cbef2bf..df2435de8 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_unambiguous.snap @@ -176,6 +176,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap index 38cbef2bf..df2435de8 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_source_version_unambiguous.snap @@ -176,6 +176,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap index 38cbef2bf..df2435de8 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__missing_dependency_version_unambiguous.snap @@ -176,6 +176,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap index 554ec8622..3624b1c56 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_has_subdir.snap @@ -85,6 +85,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap index df511a98d..1ee52e43d 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_direct_no_subdir.snap @@ -81,6 +81,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap index 60b749cac..38c5bf51b 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_directory.snap @@ -71,6 +71,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap index f90a8f7c4..c445e4a29 100644 --- a/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap +++ b/crates/uv-resolver/src/lock/snapshots/uv_resolver__lock__tests__source_editable.snap @@ -71,6 +71,7 @@ Ok( requirements: {}, constraints: {}, overrides: {}, + dependency_metadata: {}, }, }, ) diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index 565b7b119..55b1f7a94 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -2,7 +2,7 @@ use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf}; use serde::{Deserialize, Serialize}; -use distribution_types::{FlatIndexLocation, IndexUrl}; +use distribution_types::{FlatIndexLocation, IndexUrl, StaticMetadata}; use install_wheel_rs::linker::LinkMode; use pep508_rs::Requirement; use pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; @@ -288,6 +288,7 @@ pub struct ResolverOptions { pub allow_insecure_host: Option>, pub resolution: Option, pub prerelease: Option, + pub dependency_metadata: Option>, pub config_settings: Option, pub exclude_newer: Option, pub link_mode: Option, @@ -440,6 +441,29 @@ pub struct ResolverInstallerOptions { possible_values = true )] pub prerelease: Option, + /// Pre-defined static metadata for dependencies of the project (direct or transitive). When + /// provided, enables the resolver to use the specified metadata instead of querying the + /// registry or building the relevant package from source. + /// + /// Metadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) + /// standard, though only the following fields are respected: + /// + /// - `name`: The name of the package. + /// - (Optional) `version`: The version of the package. If omitted, the metadata will be applied + /// to all versions of the package. + /// - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). + /// - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). + /// - (Optional) `provides-extras`: The extras provided by the package. + #[option( + default = r#"[]"#, + value_type = "list[dict]", + example = r#" + dependency-metadata = [ + { name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" }, + ] + "# + )] + pub dependency_metadata: Option>, /// Settings to pass to the [PEP 517](https://peps.python.org/pep-0517/) build backend, /// specified as `KEY=VALUE` pairs. #[option( @@ -949,6 +973,29 @@ pub struct PipOptions { possible_values = true )] pub prerelease: Option, + /// Pre-defined static metadata for dependencies of the project (direct or transitive). When + /// provided, enables the resolver to use the specified metadata instead of querying the + /// registry or building the relevant package from source. + /// + /// Metadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) + /// standard, though only the following fields are respected: + /// + /// - `name`: The name of the package. + /// - (Optional) `version`: The version of the package. If omitted, the metadata will be applied + /// to all versions of the package. + /// - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). + /// - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). + /// - (Optional) `provides-extras`: The extras provided by the package. + #[option( + default = r#"[]"#, + value_type = "list[dict]", + example = r#" + dependency-metadata = [ + { name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" }, + ] + "# + )] + pub dependency_metadata: Option>, /// Write the requirements generated by `uv pip compile` to the given `requirements.txt` file. /// /// If the file already exists, the existing versions will be preferred when resolving @@ -1290,6 +1337,7 @@ impl From for ResolverOptions { allow_insecure_host: value.allow_insecure_host, resolution: value.resolution, prerelease: value.prerelease, + dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, exclude_newer: value.exclude_newer, link_mode: value.link_mode, @@ -1351,6 +1399,7 @@ pub struct ToolOptions { pub allow_insecure_host: Option>, pub resolution: Option, pub prerelease: Option, + pub dependency_metadata: Option>, pub config_settings: Option, pub no_build_isolation: Option, pub no_build_isolation_package: Option>, @@ -1376,6 +1425,7 @@ impl From for ToolOptions { allow_insecure_host: value.allow_insecure_host, resolution: value.resolution, prerelease: value.prerelease, + dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, no_build_isolation: value.no_build_isolation, no_build_isolation_package: value.no_build_isolation_package, @@ -1403,6 +1453,7 @@ impl From for ResolverInstallerOptions { allow_insecure_host: value.allow_insecure_host, resolution: value.resolution, prerelease: value.prerelease, + dependency_metadata: value.dependency_metadata, config_settings: value.config_settings, no_build_isolation: value.no_build_isolation, no_build_isolation_package: value.no_build_isolation_package, diff --git a/crates/uv-types/src/traits.rs b/crates/uv-types/src/traits.rs index 798b81a3c..a7dbcc337 100644 --- a/crates/uv-types/src/traits.rs +++ b/crates/uv-types/src/traits.rs @@ -4,7 +4,8 @@ use std::path::{Path, PathBuf}; use anyhow::Result; use distribution_types::{ - CachedDist, IndexCapabilities, IndexLocations, InstalledDist, Resolution, SourceDist, + CachedDist, DependencyMetadata, IndexCapabilities, IndexLocations, InstalledDist, Resolution, + SourceDist, }; use pep508_rs::PackageName; use pypi_types::Requirement; @@ -62,6 +63,9 @@ pub trait BuildContext { /// Return a reference to the discovered registry capabilities. fn capabilities(&self) -> &IndexCapabilities; + /// Return a reference to any pre-defined static metadata. + fn dependency_metadata(&self) -> &DependencyMetadata; + /// Whether source distribution building or pre-built wheels is disabled. /// /// This [`BuildContext::setup_build`] calls will fail if builds are disabled. diff --git a/crates/uv/src/commands/build.rs b/crates/uv/src/commands/build.rs index 96ebf1e68..6a2264d2b 100644 --- a/crates/uv/src/commands/build.rs +++ b/crates/uv/src/commands/build.rs @@ -115,6 +115,7 @@ async fn build_impl( allow_insecure_host, resolution: _, prerelease: _, + dependency_metadata, config_setting, no_build_isolation, no_build_isolation_package, @@ -299,6 +300,7 @@ async fn build_impl( &interpreter, index_locations, &flat_index, + dependency_metadata, &state.index, &state.git, &state.capabilities, diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index bef32ae03..60abcaefa 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -8,7 +8,7 @@ use owo_colors::OwoColorize; use tracing::debug; use distribution_types::{ - IndexCapabilities, IndexLocations, NameRequirementSpecification, + DependencyMetadata, IndexCapabilities, IndexLocations, NameRequirementSpecification, UnresolvedRequirementSpecification, Verbatim, }; use install_wheel_rs::linker::LinkMode; @@ -75,6 +75,7 @@ pub(crate) async fn pip_compile( include_index_annotation: bool, index_locations: IndexLocations, index_strategy: IndexStrategy, + dependency_metadata: DependencyMetadata, keyring_provider: KeyringProviderType, allow_insecure_host: Vec, config_settings: ConfigSettings, @@ -335,6 +336,7 @@ pub(crate) async fn pip_compile( &interpreter, &index_locations, &flat_index, + &dependency_metadata, &source_index, &git, &capabilities, diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 760b48b01..5e2954b6f 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -6,7 +6,8 @@ use owo_colors::OwoColorize; use tracing::{debug, enabled, Level}; use distribution_types::{ - IndexLocations, NameRequirementSpecification, Resolution, UnresolvedRequirementSpecification, + DependencyMetadata, IndexLocations, NameRequirementSpecification, Resolution, + UnresolvedRequirementSpecification, }; use install_wheel_rs::linker::LinkMode; use pep508_rs::PackageName; @@ -54,6 +55,7 @@ pub(crate) async fn pip_install( upgrade: Upgrade, index_locations: IndexLocations, index_strategy: IndexStrategy, + dependency_metadata: DependencyMetadata, keyring_provider: KeyringProviderType, allow_insecure_host: Vec, reinstall: Reinstall, @@ -337,6 +339,7 @@ pub(crate) async fn pip_install( interpreter, &index_locations, &flat_index, + &dependency_metadata, &state.index, &state.git, &state.capabilities, diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index aec9433e2..0721e002d 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -5,7 +5,7 @@ use anyhow::Result; use owo_colors::OwoColorize; use tracing::debug; -use distribution_types::{IndexLocations, Resolution}; +use distribution_types::{DependencyMetadata, IndexLocations, Resolution}; use install_wheel_rs::linker::LinkMode; use pep508_rs::PackageName; use uv_auth::store_credentials_from_url; @@ -47,6 +47,7 @@ pub(crate) async fn pip_sync( hash_checking: Option, index_locations: IndexLocations, index_strategy: IndexStrategy, + dependency_metadata: DependencyMetadata, keyring_provider: KeyringProviderType, allow_insecure_host: Vec, allow_empty_requirements: bool, @@ -287,6 +288,7 @@ pub(crate) async fn pip_sync( interpreter, &index_locations, &flat_index, + &dependency_metadata, &state.index, &state.git, &state.capabilities, diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 48d352715..12bddaa71 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -300,6 +300,7 @@ pub(crate) async fn add( target.interpreter(), &settings.index_locations, &flat_index, + &settings.dependency_metadata, &state.index, &state.git, &state.capabilities, diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 72c662982..054e1619f 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -9,7 +9,8 @@ use rustc_hash::{FxBuildHasher, FxHashMap}; use tracing::debug; use distribution_types::{ - IndexLocations, NameRequirementSpecification, UnresolvedRequirementSpecification, + DependencyMetadata, IndexLocations, NameRequirementSpecification, + UnresolvedRequirementSpecification, }; use pep440_rs::Version; use pypi_types::{Requirement, SupportedEnvironments}; @@ -241,6 +242,7 @@ async fn do_lock( allow_insecure_host, resolution, prerelease, + dependency_metadata, config_setting, no_build_isolation, no_build_isolation_package, @@ -409,6 +411,7 @@ async fn do_lock( interpreter, index_locations, &flat_index, + dependency_metadata, &state.index, &state.git, &state.capabilities, @@ -436,6 +439,7 @@ async fn do_lock( &constraints, &overrides, environments, + dependency_metadata, interpreter, &requires_python, index_locations, @@ -552,8 +556,14 @@ async fn do_lock( // Notify the user of any resolution diagnostics. pip::operations::diagnose_resolution(resolution.diagnostics(), printer)?; - let manifest = ResolverManifest::new(members, requirements, constraints, overrides) - .relative_to(workspace)?; + let manifest = ResolverManifest::new( + members, + requirements, + constraints, + overrides, + dependency_metadata.values().cloned(), + ) + .relative_to(workspace)?; let previous = existing_lock.map(ValidatedLock::into_lock); let lock = Lock::from_resolution_graph(&resolution, workspace.install_path())? @@ -591,6 +601,7 @@ impl ValidatedLock { constraints: &[Requirement], overrides: &[Requirement], environments: Option<&SupportedEnvironments>, + dependency_metadata: &DependencyMetadata, interpreter: &Interpreter, requires_python: &RequiresPython, index_locations: &IndexLocations, @@ -710,6 +721,7 @@ impl ValidatedLock { requirements, constraints, overrides, + dependency_metadata, indexes, build_options, interpreter.tags()?, @@ -773,6 +785,13 @@ impl ValidatedLock { ); Ok(Self::Preferable(lock)) } + SatisfiesResult::MismatchedStaticMetadata(expected, actual) => { + debug!( + "Ignoring existing lockfile due to mismatched static metadata:\n Expected: {:?}\n Actual: {:?}", + expected, actual + ); + Ok(Self::Preferable(lock)) + } SatisfiesResult::MissingRoot(name) => { debug!("Ignoring existing lockfile due to missing root package: `{name}`"); Ok(Self::Preferable(lock)) diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 8fbc3827f..56907f74e 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -575,6 +575,7 @@ pub(crate) async fn resolve_names( allow_insecure_host, resolution: _, prerelease: _, + dependency_metadata, config_setting, no_build_isolation, no_build_isolation_package, @@ -631,6 +632,7 @@ pub(crate) async fn resolve_names( interpreter, index_locations, &flat_index, + dependency_metadata, &state.index, &state.git, &state.capabilities, @@ -684,6 +686,7 @@ pub(crate) async fn resolve_environment<'a>( allow_insecure_host, resolution, prerelease, + dependency_metadata, config_setting, no_build_isolation, no_build_isolation_package, @@ -774,6 +777,7 @@ pub(crate) async fn resolve_environment<'a>( interpreter, index_locations, &flat_index, + dependency_metadata, &state.index, &state.git, &state.capabilities, @@ -837,6 +841,7 @@ pub(crate) async fn sync_environment( index_strategy, keyring_provider, allow_insecure_host, + dependency_metadata, config_setting, no_build_isolation, no_build_isolation_package, @@ -903,6 +908,7 @@ pub(crate) async fn sync_environment( interpreter, index_locations, &flat_index, + dependency_metadata, &state.index, &state.git, &state.capabilities, @@ -989,6 +995,7 @@ pub(crate) async fn update_environment( allow_insecure_host, resolution, prerelease, + dependency_metadata, config_setting, no_build_isolation, no_build_isolation_package, @@ -1104,6 +1111,7 @@ pub(crate) async fn update_environment( interpreter, index_locations, &flat_index, + dependency_metadata, &state.index, &state.git, &state.capabilities, diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index af46bfeb1..f40723571 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -180,6 +180,7 @@ pub(super) async fn do_sync( index_strategy, keyring_provider, allow_insecure_host, + dependency_metadata, config_setting, no_build_isolation, no_build_isolation_package, @@ -304,6 +305,7 @@ pub(super) async fn do_sync( venv.interpreter(), index_locations, &flat_index, + dependency_metadata, &state.index, &state.git, &state.capabilities, diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 45c64102e..7f410d92a 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -9,7 +9,7 @@ use miette::{Diagnostic, IntoDiagnostic}; use owo_colors::OwoColorize; use thiserror::Error; -use distribution_types::IndexLocations; +use distribution_types::{DependencyMetadata, IndexLocations}; use install_wheel_rs::linker::LinkMode; use pypi_types::Requirement; use uv_auth::store_credentials_from_url; @@ -48,6 +48,7 @@ pub(crate) async fn venv( link_mode: LinkMode, index_locations: &IndexLocations, index_strategy: IndexStrategy, + dependency_metadata: DependencyMetadata, keyring_provider: KeyringProviderType, allow_insecure_host: Vec, prompt: uv_virtualenv::Prompt, @@ -70,6 +71,7 @@ pub(crate) async fn venv( link_mode, index_locations, index_strategy, + dependency_metadata, keyring_provider, allow_insecure_host, prompt, @@ -125,6 +127,7 @@ async fn venv_impl( link_mode: LinkMode, index_locations: &IndexLocations, index_strategy: IndexStrategy, + dependency_metadata: DependencyMetadata, keyring_provider: KeyringProviderType, allow_insecure_host: Vec, prompt: uv_virtualenv::Prompt, @@ -317,6 +320,7 @@ async fn venv_impl( interpreter, index_locations, &flat_index, + &dependency_metadata, &state.index, &state.git, &state.capabilities, diff --git a/crates/uv/src/lib.rs b/crates/uv/src/lib.rs index d7fc0e06e..80b788ec2 100644 --- a/crates/uv/src/lib.rs +++ b/crates/uv/src/lib.rs @@ -323,6 +323,7 @@ async fn run(cli: Cli) -> Result { args.settings.emit_index_annotation, args.settings.index_locations, args.settings.index_strategy, + args.settings.dependency_metadata, args.settings.keyring_provider, args.settings.allow_insecure_host, args.settings.config_setting, @@ -390,6 +391,7 @@ async fn run(cli: Cli) -> Result { args.settings.hash_checking, args.settings.index_locations, args.settings.index_strategy, + args.settings.dependency_metadata, args.settings.keyring_provider, args.settings.allow_insecure_host, args.settings.allow_empty_requirements, @@ -453,7 +455,6 @@ async fn run(cli: Cli) -> Result { .into_iter() .map(RequirementsSource::from_overrides_txt) .collect::>(); - let build_constraints = args .build_constraint .into_iter() @@ -474,6 +475,7 @@ async fn run(cli: Cli) -> Result { args.settings.upgrade, args.settings.index_locations, args.settings.index_strategy, + args.settings.dependency_metadata, args.settings.keyring_provider, args.settings.allow_insecure_host, args.settings.reinstall, @@ -733,6 +735,7 @@ async fn run(cli: Cli) -> Result { args.settings.link_mode, &args.settings.index_locations, args.settings.index_strategy, + args.settings.dependency_metadata, args.settings.keyring_provider, args.settings.allow_insecure_host, uv_virtualenv::Prompt::from_args(prompt), @@ -884,7 +887,7 @@ async fn run(cli: Cli) -> Result { ) .collect::>(); - commands::tool_install( + Box::pin(commands::tool_install( args.package, args.editable, args.from, @@ -900,7 +903,7 @@ async fn run(cli: Cli) -> Result { globals.native_tls, cache, printer, - ) + )) .await } Commands::Tool(ToolNamespace { diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index ae06f015c..c7d1eb626 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use std::process; use std::str::FromStr; -use distribution_types::IndexLocations; +use distribution_types::{DependencyMetadata, IndexLocations}; use install_wheel_rs::linker::LinkMode; use pep508_rs::{ExtraName, RequirementOrigin}; use pypi_types::{Requirement, SupportedEnvironments}; @@ -1784,6 +1784,7 @@ pub(crate) struct InstallerSettingsRef<'a> { pub(crate) index_strategy: IndexStrategy, pub(crate) keyring_provider: KeyringProviderType, pub(crate) allow_insecure_host: &'a [TrustedHost], + pub(crate) dependency_metadata: &'a DependencyMetadata, pub(crate) config_setting: &'a ConfigSettings, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: &'a [PackageName], @@ -1808,6 +1809,7 @@ pub(crate) struct ResolverSettings { pub(crate) allow_insecure_host: Vec, pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) dependency_metadata: DependencyMetadata, pub(crate) config_setting: ConfigSettings, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: Vec, @@ -1826,6 +1828,7 @@ pub(crate) struct ResolverSettingsRef<'a> { pub(crate) allow_insecure_host: &'a [TrustedHost], pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) dependency_metadata: &'a DependencyMetadata, pub(crate) config_setting: &'a ConfigSettings, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: &'a [PackageName], @@ -1857,6 +1860,7 @@ impl ResolverSettings { allow_insecure_host: &self.allow_insecure_host, resolution: self.resolution, prerelease: self.prerelease, + dependency_metadata: &self.dependency_metadata, config_setting: &self.config_setting, no_build_isolation: self.no_build_isolation, no_build_isolation_package: &self.no_build_isolation_package, @@ -1880,6 +1884,9 @@ impl From for ResolverSettings { ), resolution: value.resolution.unwrap_or_default(), prerelease: value.prerelease.unwrap_or_default(), + dependency_metadata: DependencyMetadata::from_entries( + value.dependency_metadata.into_iter().flatten(), + ), index_strategy: value.index_strategy.unwrap_or_default(), keyring_provider: value.keyring_provider.unwrap_or_default(), allow_insecure_host: value.allow_insecure_host.unwrap_or_default(), @@ -1914,6 +1921,7 @@ pub(crate) struct ResolverInstallerSettingsRef<'a> { pub(crate) allow_insecure_host: &'a [TrustedHost], pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) dependency_metadata: &'a DependencyMetadata, pub(crate) config_setting: &'a ConfigSettings, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: &'a [PackageName], @@ -1932,8 +1940,7 @@ pub(crate) struct ResolverInstallerSettingsRef<'a> { /// Represents the shared settings that are used across all uv commands outside the `pip` API. /// Analogous to the settings contained in the `[tool.uv]` table, combined with [`ResolverInstallerArgs`]. #[allow(clippy::struct_excessive_bools)] -#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)] -#[serde(rename_all = "kebab-case", deny_unknown_fields)] +#[derive(Debug, Clone, Default)] pub(crate) struct ResolverInstallerSettings { pub(crate) index_locations: IndexLocations, pub(crate) index_strategy: IndexStrategy, @@ -1941,6 +1948,7 @@ pub(crate) struct ResolverInstallerSettings { pub(crate) allow_insecure_host: Vec, pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) dependency_metadata: DependencyMetadata, pub(crate) config_setting: ConfigSettings, pub(crate) no_build_isolation: bool, pub(crate) no_build_isolation_package: Vec, @@ -1977,6 +1985,7 @@ impl ResolverInstallerSettings { allow_insecure_host: &self.allow_insecure_host, resolution: self.resolution, prerelease: self.prerelease, + dependency_metadata: &self.dependency_metadata, config_setting: &self.config_setting, no_build_isolation: self.no_build_isolation, no_build_isolation_package: &self.no_build_isolation_package, @@ -2002,6 +2011,9 @@ impl From for ResolverInstallerSettings { ), resolution: value.resolution.unwrap_or_default(), prerelease: value.prerelease.unwrap_or_default(), + dependency_metadata: DependencyMetadata::from_entries( + value.dependency_metadata.into_iter().flatten(), + ), index_strategy: value.index_strategy.unwrap_or_default(), keyring_provider: value.keyring_provider.unwrap_or_default(), allow_insecure_host: value.allow_insecure_host.unwrap_or_default(), @@ -2058,6 +2070,7 @@ pub(crate) struct PipSettings { pub(crate) dependency_mode: DependencyMode, pub(crate) resolution: ResolutionMode, pub(crate) prerelease: PrereleaseMode, + pub(crate) dependency_metadata: DependencyMetadata, pub(crate) output_file: Option, pub(crate) no_strip_extras: bool, pub(crate) no_strip_markers: bool, @@ -2117,6 +2130,7 @@ impl PipSettings { allow_empty_requirements, resolution, prerelease, + dependency_metadata, output_file, no_strip_extras, no_strip_markers, @@ -2157,6 +2171,7 @@ impl PipSettings { allow_insecure_host: top_level_allow_insecure_host, resolution: top_level_resolution, prerelease: top_level_prerelease, + dependency_metadata: top_level_dependency_metadata, config_settings: top_level_config_settings, no_build_isolation: top_level_no_build_isolation, no_build_isolation_package: top_level_no_build_isolation_package, @@ -2187,6 +2202,7 @@ impl PipSettings { let allow_insecure_host = allow_insecure_host.combine(top_level_allow_insecure_host); let resolution = resolution.combine(top_level_resolution); let prerelease = prerelease.combine(top_level_prerelease); + let dependency_metadata = dependency_metadata.combine(top_level_dependency_metadata); let config_settings = config_settings.combine(top_level_config_settings); let no_build_isolation = no_build_isolation.combine(top_level_no_build_isolation); let no_build_isolation_package = @@ -2220,6 +2236,11 @@ impl PipSettings { }, resolution: args.resolution.combine(resolution).unwrap_or_default(), prerelease: args.prerelease.combine(prerelease).unwrap_or_default(), + dependency_metadata: DependencyMetadata::from_entries( + args.dependency_metadata + .combine(dependency_metadata) + .unwrap_or_default(), + ), output_file: args.output_file.combine(output_file), no_strip_extras: args .no_strip_extras @@ -2364,6 +2385,7 @@ impl<'a> From> for ResolverSettingsRef<'a> { allow_insecure_host: settings.allow_insecure_host, resolution: settings.resolution, prerelease: settings.prerelease, + dependency_metadata: settings.dependency_metadata, config_setting: settings.config_setting, no_build_isolation: settings.no_build_isolation, no_build_isolation_package: settings.no_build_isolation_package, @@ -2383,6 +2405,7 @@ impl<'a> From> for InstallerSettingsRef<'a> { index_strategy: settings.index_strategy, keyring_provider: settings.keyring_provider, allow_insecure_host: settings.allow_insecure_host, + dependency_metadata: settings.dependency_metadata, config_setting: settings.config_setting, no_build_isolation: settings.no_build_isolation, no_build_isolation_package: settings.no_build_isolation_package, diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index c9fc1d4fd..ec1f75c70 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -12627,6 +12627,224 @@ fn lock_python_upper_bound() -> Result<()> { Ok(()) } +#[test] +fn lock_dependency_metadata() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [[tool.uv.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["iniconfig"] + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [manifest] + + [[manifest.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["iniconfig"] + + [[package]] + name = "anyio" + version = "3.7.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "iniconfig" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + ] + + [package.metadata] + requires-dist = [{ name = "anyio", specifier = "==3.7.0" }] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + // Re-run with `--offline`. We shouldn't need a network connection to validate an + // already-correct lockfile with immutable metadata. + uv_snapshot!(context.filters(), context.lock().arg("--locked").arg("--offline").arg("--no-cache"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + "###); + + // Install from the lockfile. + uv_snapshot!(context.filters(), context.sync().arg("--frozen"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Prepared 3 packages in [TIME] + Installed 3 packages in [TIME] + + anyio==3.7.0 + + iniconfig==2.0.0 + + project==0.1.0 (from file://[TEMP_DIR]/) + "###); + + // Update the static metadata. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [[tool.uv.dependency-metadata]] + name = "anyio" + version = "3.7.0" + requires-dist = ["typing-extensions"] + "#, + )?; + + // The lockfile should update. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Removed iniconfig v2.0.0 + Added typing-extensions v4.10.0 + "###); + + // Remove the static metadata. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + "#, + )?; + + // The lockfile should update. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + Added idna v3.6 + Added sniffio v1.3.1 + Removed typing-extensions v4.10.0 + "###); + + // Use a blanket match. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [[tool.uv.dependency-metadata]] + name = "anyio" + requires-dist = ["iniconfig"] + "#, + )?; + + // The lockfile should update. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 3 packages in [TIME] + Removed idna v3.6 + Added iniconfig v2.0.0 + Removed sniffio v1.3.1 + "###); + + Ok(()) +} + #[test] fn lock_strip_fragment() -> Result<()> { let context = TestContext::new("3.12"); diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs index 099e90529..cf402a103 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -140,6 +140,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -279,6 +282,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -419,6 +425,9 @@ fn resolve_uv_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -591,6 +600,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -709,6 +721,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -859,6 +874,9 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -1046,6 +1064,9 @@ fn resolve_index_url() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -1232,6 +1253,9 @@ fn resolve_index_url() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -1396,6 +1420,9 @@ fn resolve_find_links() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -1536,6 +1563,9 @@ fn resolve_top_level() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -1714,6 +1744,9 @@ fn resolve_top_level() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -1875,6 +1908,9 @@ fn resolve_top_level() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -2015,6 +2051,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -2138,6 +2177,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -2261,6 +2303,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -2386,6 +2431,9 @@ fn resolve_user_configuration() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -2509,6 +2557,7 @@ fn resolve_tool() -> anyhow::Result<()> { LowestDirect, ), prerelease: None, + dependency_metadata: None, config_settings: None, no_build_isolation: None, no_build_isolation_package: None, @@ -2543,6 +2592,9 @@ fn resolve_tool() -> anyhow::Result<()> { allow_insecure_host: [], resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), config_setting: ConfigSettings( {}, ), @@ -2683,6 +2735,9 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -2857,6 +2912,9 @@ fn resolve_both() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -3022,6 +3080,9 @@ fn resolve_config_file() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -3240,6 +3301,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: LowestDirect, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -3366,6 +3430,9 @@ fn resolve_skip_empty() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, @@ -3511,6 +3578,9 @@ fn allow_insecure_host() -> anyhow::Result<()> { dependency_mode: Transitive, resolution: Highest, prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), output_file: None, no_strip_extras: false, no_strip_markers: false, diff --git a/docs/concepts/projects.md b/docs/concepts/projects.md index cb98cf2d0..15db69cca 100644 --- a/docs/concepts/projects.md +++ b/docs/concepts/projects.md @@ -715,8 +715,8 @@ $ uv sync --extra build $ uv sync --extra build --extra compile ``` -Some packages, like `cchardet`, only require build dependencies for the _installation_ phase of -`uv sync`. Others, like `flash-attn`, require their build dependencies to be present even just to +Some packages, like `cchardet` above, only require build dependencies for the _installation_ phase +of `uv sync`. Others, like `flash-attn`, require their build dependencies to be present even just to resolve the project's lockfile during the _resolution_ phase. In such cases, the build dependencies must be installed prior to running any `uv lock` or `uv sync` @@ -735,10 +735,64 @@ dependencies = ["flash-attn"] no-build-isolation-package = ["flash-attn"] ``` -You could run the following sequence of commands: +You could run the following sequence of commands to sync `flash-attn`: ```console $ uv venv $ uv pip install torch $ uv sync ``` + +Alternatively, you can provide the `flash-attn` metadata upfront via the +[`dependency-metadata`](../reference/settings.md#dependency-metadata) setting, thereby forgoing the +need to build the package during the dependency resolution phase. For example, to provide the +`flash-attn` metadata upfront, include the following in your `pyproject.toml`: + +```toml title="pyproject.toml" +[[tool.uv.dependency-metadata]] +name = "flash-attn" +version = "2.6.3" +requires-dist = ["torch", "einops"] +``` + +!!! tip + + To determine the package metadata for a package like `flash-attn`, navigate to the appropriate Git repository, + or look it up on [PyPI](https://pypi.org/project/flash-attn) and download the package's source distribution. + The package requirements can typically be found in the `setup.py` or `setup.cfg` file. + + (If the package includes a built distribution, you can unzip it to find the `METADATA` file; however, the presence + of a built distribution would negate the need to provide the metadata upfront, since it would already be available + to uv.) + +Once included, you can again use the two-step `uv sync` process to install the build dependencies. +Given the following `pyproject.toml`: + +```toml title="pyproject.toml" +[project] +name = "project" +version = "0.1.0" +description = "..." +readme = "README.md" +requires-python = ">=3.12" +dependencies = [] + +[project.optional-dependencies] +build = ["torch", "setuptools", "packaging"] +compile = ["flash-attn"] + +[tool.uv] +no-build-isolation-package = ["flash-attn"] + +[[tool.uv.dependency-metadata]] +name = "flash-attn" +version = "2.6.3" +requires-dist = ["torch", "einops"] +``` + +You could run the following sequence of commands to sync `flash-attn`: + +```console +$ uv sync --extra build +$ uv sync --extra build --extra compile +``` diff --git a/docs/concepts/resolution.md b/docs/concepts/resolution.md index 9681615fa..4e4583e82 100644 --- a/docs/concepts/resolution.md +++ b/docs/concepts/resolution.md @@ -227,28 +227,35 @@ For more details, see ## Dependency constraints -uv supports constraints files (`--constraint constraints.txt`), like pip, which narrow the set of -acceptable versions for the given packages. Constraint files are like a regular requirements files, -but they do not add packages to the requirements — they only take affect if the package is requested -in a direct or transitive dependency. Constraints are often useful for reducing the range of -available versions for a transitive dependency without adding a direct requirement on the package. +Like pip, uv supports constraint files (`--constraint constraints.txt`) which narrow the set of +acceptable versions for the given packages. Constraint files are similar to requirements files, but +being listed as a constraint alone will not cause a package to be included to the resolution. +Instead, constraints only take effect if a requested package is already pulled in as a direct or +transitive dependency. Constraints are useful for reducing the range of available versions for a +transitive dependency. They can also be used to keep a resolution in sync with some other set of +resolved versions, regardless of which packages are overlapping between the two. ## Dependency overrides -Overrides allow bypassing failing or undesirable resolutions by overriding the declared dependencies -of a package. Overrides are a useful last resort for cases in which the you know that a dependency -is compatible with a newer version of a package than it declares, but the it has not yet been -updated to declare that compatibility. +Dependency overrides allow bypassing failing or undesirable resolutions by overriding a package's +declared dependencies. Overrides are a useful last resort for cases in which you _know_ that a +dependency is compatible with a certain version of a package, despite the metadata indicating +otherwise. -For example, if a transitive dependency declares the requirement `pydantic>=1.0,<2.0`, but _works_ -with `pydantic>=2.0`, the user can override the declared dependency with `pydantic>=1.0,<3` to allow -the resolver to installer a newer version of `pydantic`. +For example, if a transitive dependency declares the requirement `pydantic>=1.0,<2.0`, but _does_ +work with `pydantic>=2.0`, the user can override the declared dependency by including +`pydantic>=1.0,<3` in the overrides, thereby allowing the resolver to choose a newer version of +`pydantic`. -While constraints and dependencies are purely additive, and thus cannot expand the set of acceptable -versions for a package, overrides can expand the set of acceptable versions for a package, providing -an escape hatch for erroneous upper version bounds. As with constraints, overrides do not add a -dependency on the package and only take affect if the package is requested in a direct or transitive -dependency. +Concretely, if `pydantic>=1.0,<3` is included as an override, uv will ignore all declared +requirements on `pydantic`, replacing them with the override. In the above example, the +`pydantic>=1.0,<2.0` requirement would be ignored completely, and would instead be replaced with +`pydantic>=1.0,<3`. + +While constraints can only _reduce_ the set of acceptable versions for a package, overrides can +_expand_ the set of acceptable versions, providing an escape hatch for erroneous upper version +bounds. As with constraints, overrides do not add a dependency on the package and only take effect +if the package is requested in a direct or transitive dependency. In a `pyproject.toml`, use `tool.uv.override-dependencies` to define a list of overrides. In the pip-compatible interface, the `--override` option can be used to pass files with the same format as @@ -258,6 +265,60 @@ If multiple overrides are provided for the same package, they must be differenti [markers](#platform-markers). If a package has a dependency with a marker, it is replaced unconditionally when using overrides — it does not matter if the marker evaluates to true or false. +## Dependency metadata + +During resolution, uv needs to resolve the metadata for each package it encounters, in order to +determine its dependencies. This metadata is often available as a static file in the package index; +however, for packages that only provide source distributions, the metadata may not be available +upfront. + +In such cases, uv has to build the package to determine its metadata (e.g., by invoking `setup.py`). +This can introduce a performance penalty during resolution. Further, it imposes the requirement that +the package can be built on all platforms, which may not be true. + +For example, you may have a package that should only be built and installed on Linux, but doesn't +build successfully on macOS or Windows. While uv can construct a perfectly valid lockfile for this +scenario, doing so would require building the package, which would fail on non-Linux platforms. + +The `tool.uv.dependency-metadata` table can be used to provide static metadata for such dependencies +upfront, thereby allowing uv to skip the build step and use the provided metadata instead. + +For example, to provide metadata for `chumpy` upfront, include its `dependency-metadata` in the +`pyproject.toml`: + +```toml +[[tool.uv.dependency-metadata]] +name = "chumpy" +version = "0.70" +requires-dist = ["numpy>=1.8.1", "scipy>=0.13.0", "six>=1.11.0"] +``` + +These declarations are intended for cases in which a package does _not_ declare static metadata +upfront, though they are also useful for packages that require disabling build isolation. In such +cases, it may be easier to declare the package metadata upfront, rather than creating a custom build +environment prior to resolving the package. + +For example, you can declare the metadata for `flash-attn`, allowing uv to resolve without building +the package from source (which itself requires installing `torch`): + +```toml +[[tool.uv.dependency-metadata]] +name = "flash-attn" +version = "2.6.3" +requires-dist = ["torch", "einops"] +``` + +Like dependency overrides, `tool.uv.dependency-metadata` can also be used for cases in which a +package's metadata is incorrect or incomplete, or when a package is not available in the package +index. While dependency overrides allow overriding the allowed versions of a package globally, +metadata overrides allow overriding the declared metadata of a _specific package_. + +Entries in the `tool.uv.dependency-metadata` table follow the +[Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) specification, +though only `name`, `version`, `requires-dist`, `requires-python`, and `provides-extra` are read by +uv. The `version` field is also considered optional. If omitted, the metadata will be used for all +versions of the specified package. + ## Lower bounds By default, `uv add` adds lower bounds to dependencies and, when using uv to manage projects, uv diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 146340a00..f71c47405 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -451,6 +451,47 @@ specified as `KEY=VALUE` pairs. --- +### [`dependency-metadata`](#dependency-metadata) {: #dependency-metadata } + +Pre-defined static metadata for dependencies of the project (direct or transitive). When +provided, enables the resolver to use the specified metadata instead of querying the +registry or building the relevant package from source. + +Metadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) +standard, though only the following fields are respected: + +- `name`: The name of the package. +- (Optional) `version`: The version of the package. If omitted, the metadata will be applied + to all versions of the package. +- (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). +- (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). +- (Optional) `provides-extras`: The extras provided by the package. + +**Default value**: `[]` + +**Type**: `list[dict]` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv] + dependency-metadata = [ + { name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" }, + ] + ``` +=== "uv.toml" + + ```toml + + dependency-metadata = [ + { name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" }, + ] + ``` + +--- + ### [`exclude-newer`](#exclude-newer) {: #exclude-newer } Limit candidate packages to those that were uploaded prior to the given date. @@ -1474,6 +1515,48 @@ Used to reflect custom build scripts and commands that wrap `uv pip compile`. --- +#### [`dependency-metadata`](#pip_dependency-metadata) {: #pip_dependency-metadata } + + +Pre-defined static metadata for dependencies of the project (direct or transitive). When +provided, enables the resolver to use the specified metadata instead of querying the +registry or building the relevant package from source. + +Metadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) +standard, though only the following fields are respected: + +- `name`: The name of the package. +- (Optional) `version`: The version of the package. If omitted, the metadata will be applied + to all versions of the package. +- (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). +- (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). +- (Optional) `provides-extras`: The extras provided by the package. + +**Default value**: `[]` + +**Type**: `list[dict]` + +**Example usage**: + +=== "pyproject.toml" + + ```toml + [tool.uv.pip] + dependency-metadata = [ + { name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" }, + ] + ``` +=== "uv.toml" + + ```toml + [pip] + dependency-metadata = [ + { name = "flask", version = "1.0.0", requires-dist = ["werkzeug"], requires-python = ">=3.6" }, + ] + ``` + +--- + #### [`emit-build-options`](#pip_emit-build-options) {: #pip_emit-build-options } diff --git a/uv.schema.json b/uv.schema.json index 50d564251..cafce3cae 100644 --- a/uv.schema.json +++ b/uv.schema.json @@ -87,6 +87,16 @@ "type": "string" } }, + "dependency-metadata": { + "description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/StaticMetadata" + } + }, "dev-dependencies": { "description": "PEP 508-style requirements, e.g., `ruff==0.5.0`, or `ruff @ https://...`.", "type": [ @@ -677,6 +687,16 @@ "null" ] }, + "dependency-metadata": { + "description": "Pre-defined static metadata for dependencies of the project (direct or transitive). When provided, enables the resolver to use the specified metadata instead of querying the registry or building the relevant package from source.\n\nMetadata should be provided in adherence with the [Metadata 2.3](https://packaging.python.org/en/latest/specifications/core-metadata/) standard, though only the following fields are respected:\n\n- `name`: The name of the package. - (Optional) `version`: The version of the package. If omitted, the metadata will be applied to all versions of the package. - (Optional) `requires-dist`: The dependencies of the package (e.g., `werkzeug>=0.14`). - (Optional) `requires-python`: The Python version required by the package (e.g., `>=3.10`). - (Optional) `provides-extras`: The extras provided by the package.", + "type": [ + "array", + "null" + ], + "items": { + "$ref": "#/definitions/StaticMetadata" + } + }, "emit-build-options": { "description": "Include `--no-binary` and `--only-binary` entries in the output file generated by `uv pip compile`.", "type": [ @@ -1153,7 +1173,7 @@ "pattern": "^3\\.\\d+(\\.\\d+)?$" }, "Requirement": { - "description": "A PEP 508 dependency specifier", + "description": "A PEP 508 dependency specifier, e.g., `ruff >= 0.6.0`", "type": "string" }, "ResolutionMode": { @@ -1347,6 +1367,44 @@ } ] }, + "StaticMetadata": { + "description": "A subset of the Python Package Metadata 2.3 standard as specified in .", + "type": "object", + "required": [ + "name", + "version" + ], + "properties": { + "name": { + "$ref": "#/definitions/PackageName" + }, + "provides-extras": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/ExtraName" + } + }, + "requires-dist": { + "default": [], + "type": "array", + "items": { + "$ref": "#/definitions/Requirement" + } + }, + "requires-python": { + "description": "PEP 508-style Python requirement, e.g., `>=3.10`", + "type": [ + "string", + "null" + ] + }, + "version": { + "description": "PEP 440-style package version, e.g., `1.2.3`", + "type": "string" + } + } + }, "String": { "type": "string" },