From c6567542eea5f6a55bd366eeec41db36e19beef3 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Sat, 28 Sep 2024 13:52:42 -0400 Subject: [PATCH] Allow multiple indexes --- crates/uv-resolver/src/error.rs | 10 + crates/uv-resolver/src/fork_indexes.rs | 48 ++ crates/uv-resolver/src/fork_urls.rs | 2 +- crates/uv-resolver/src/lib.rs | 1 + crates/uv-resolver/src/resolution/graph.rs | 79 +++- .../src/resolver/batch_prefetch.rs | 25 +- crates/uv-resolver/src/resolver/fork_map.rs | 5 + crates/uv-resolver/src/resolver/index.rs | 15 +- crates/uv-resolver/src/resolver/indexes.rs | 46 +- crates/uv-resolver/src/resolver/mod.rs | 125 +++++- crates/uv-workspace/src/pyproject.rs | 12 - crates/uv/tests/it/lock.rs | 411 +++++++++++++++++- docs/concepts/dependencies.md | 27 +- docs/configuration/indexes.md | 22 + 14 files changed, 717 insertions(+), 111 deletions(-) create mode 100644 crates/uv-resolver/src/fork_indexes.rs diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index 786e3b6e3c4e..9824786d5324 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -53,6 +53,16 @@ pub enum ResolveError { fork_markers: MarkerTree, }, + #[error("Requirements contain conflicting indexes for package `{0}`:\n- {}", _1.join("\n- "))] + ConflictingIndexesUniversal(PackageName, Vec), + + #[error("Requirements contain conflicting indexes for package `{package_name}` in split `{fork_markers:?}`:\n- {}", indexes.join("\n- "))] + ConflictingIndexesFork { + package_name: PackageName, + indexes: Vec, + fork_markers: MarkerTree, + }, + #[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")] ConflictingIndexes(PackageName, String, String), diff --git a/crates/uv-resolver/src/fork_indexes.rs b/crates/uv-resolver/src/fork_indexes.rs new file mode 100644 index 000000000000..48af8fbe3c08 --- /dev/null +++ b/crates/uv-resolver/src/fork_indexes.rs @@ -0,0 +1,48 @@ +use rustc_hash::FxHashMap; +use uv_distribution_types::IndexUrl; +use uv_normalize::PackageName; + +use crate::resolver::ResolverMarkers; +use crate::ResolveError; + +/// See [`crate::resolver::ForkState`]. +#[derive(Default, Debug, Clone)] +pub(crate) struct ForkIndexes(FxHashMap); + +impl ForkIndexes { + /// Get the [`IndexUrl`] previously used for a package in this fork. + pub(crate) fn get(&self, package_name: &PackageName) -> Option<&IndexUrl> { + self.0.get(package_name) + } + + /// Check that this is the only [`IndexUrl`] used for this package in this fork. + pub(crate) fn insert( + &mut self, + package_name: &PackageName, + index: &IndexUrl, + fork_markers: &ResolverMarkers, + ) -> Result<(), ResolveError> { + if let Some(previous) = self.0.insert(package_name.clone(), index.clone()) { + if &previous != index { + let mut conflicts = vec![previous.to_string(), index.to_string()]; + conflicts.sort(); + return match fork_markers { + ResolverMarkers::Universal { .. } | ResolverMarkers::SpecificEnvironment(_) => { + Err(ResolveError::ConflictingIndexesUniversal( + package_name.clone(), + conflicts, + )) + } + ResolverMarkers::Fork(fork_markers) => { + Err(ResolveError::ConflictingIndexesFork { + package_name: package_name.clone(), + indexes: conflicts, + fork_markers: fork_markers.clone(), + }) + } + }; + } + } + Ok(()) + } +} diff --git a/crates/uv-resolver/src/fork_urls.rs b/crates/uv-resolver/src/fork_urls.rs index 39edf0e9bebf..5b11f21178de 100644 --- a/crates/uv-resolver/src/fork_urls.rs +++ b/crates/uv-resolver/src/fork_urls.rs @@ -9,7 +9,7 @@ use uv_pypi_types::VerbatimParsedUrl; use crate::resolver::ResolverMarkers; use crate::ResolveError; -/// See [`crate::resolver::SolveState`]. +/// See [`crate::resolver::ForkState`]. #[derive(Default, Debug, Clone)] pub(crate) struct ForkUrls(FxHashMap); diff --git a/crates/uv-resolver/src/lib.rs b/crates/uv-resolver/src/lib.rs index 91f4a025d06c..4d60b9f95cac 100644 --- a/crates/uv-resolver/src/lib.rs +++ b/crates/uv-resolver/src/lib.rs @@ -34,6 +34,7 @@ mod error; mod exclude_newer; mod exclusions; mod flat_index; +mod fork_indexes; mod fork_urls; mod graph_ops; mod lock; diff --git a/crates/uv-resolver/src/resolution/graph.rs b/crates/uv-resolver/src/resolution/graph.rs index f8d3a8fe9cb4..ff8a540de89a 100644 --- a/crates/uv-resolver/src/resolution/graph.rs +++ b/crates/uv-resolver/src/resolution/graph.rs @@ -10,7 +10,7 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet}; use uv_configuration::{Constraints, Overrides}; use uv_distribution::Metadata; use uv_distribution_types::{ - Dist, DistributionMetadata, Name, ResolutionDiagnostic, ResolvedDist, VersionId, + Dist, DistributionMetadata, IndexUrl, Name, ResolutionDiagnostic, ResolvedDist, VersionId, VersionOrUrlRef, }; use uv_git::GitResolver; @@ -88,6 +88,7 @@ struct PackageRef<'a> { package_name: &'a PackageName, version: &'a Version, url: Option<&'a VerbatimParsedUrl>, + index: Option<&'a IndexUrl>, extra: Option<&'a ExtraName>, group: Option<&'a GroupName>, } @@ -284,6 +285,7 @@ impl ResolutionGraph { package_name: from, version: &edge.from_version, url: edge.from_url.as_ref(), + index: edge.from_index.as_ref(), extra: edge.from_extra.as_ref(), group: edge.from_dev.as_ref(), }] @@ -292,6 +294,7 @@ impl ResolutionGraph { package_name: &edge.to, version: &edge.to_version, url: edge.to_url.as_ref(), + index: edge.to_index.as_ref(), extra: edge.to_extra.as_ref(), group: edge.to_dev.as_ref(), }]; @@ -320,7 +323,7 @@ impl ResolutionGraph { diagnostics: &mut Vec, preferences: &Preferences, pins: &FilePins, - index: &InMemoryIndex, + in_memory: &InMemoryIndex, git: &GitResolver, package: &'a ResolutionPackage, version: &'a Version, @@ -330,16 +333,18 @@ impl ResolutionGraph { extra, dev, url, + index, } = &package; // Map the package to a distribution. let (dist, hashes, metadata) = Self::parse_dist( name, + index.as_ref(), url.as_ref(), version, pins, diagnostics, preferences, - index, + in_memory, git, )?; @@ -366,7 +371,7 @@ impl ResolutionGraph { } // Add the distribution to the graph. - let index = petgraph.add_node(ResolutionGraphNode::Dist(AnnotatedDist { + let node = petgraph.add_node(ResolutionGraphNode::Dist(AnnotatedDist { dist, name: name.clone(), version: version.clone(), @@ -381,22 +386,24 @@ impl ResolutionGraph { package_name: name, version, url: url.as_ref(), + index: index.as_ref(), extra: extra.as_ref(), group: dev.as_ref(), }, - index, + node, ); Ok(()) } fn parse_dist( name: &PackageName, + index: Option<&IndexUrl>, url: Option<&VerbatimParsedUrl>, version: &Version, pins: &FilePins, diagnostics: &mut Vec, preferences: &Preferences, - index: &InMemoryIndex, + in_memory: &InMemoryIndex, git: &GitResolver, ) -> Result<(ResolvedDist, Vec, Option), ResolveError> { Ok(if let Some(url) = url { @@ -406,14 +413,24 @@ impl ResolutionGraph { let version_id = VersionId::from_url(&url.verbatim); // Extract the hashes. - let hashes = - Self::get_hashes(name, Some(url), &version_id, version, preferences, index); + let hashes = Self::get_hashes( + name, + index, + Some(url), + &version_id, + version, + preferences, + in_memory, + ); // Extract the metadata. let metadata = { - let response = index.distributions().get(&version_id).unwrap_or_else(|| { - panic!("Every URL distribution should have metadata: {version_id:?}") - }); + let response = in_memory + .distributions() + .get(&version_id) + .unwrap_or_else(|| { + panic!("Every URL distribution should have metadata: {version_id:?}") + }); let MetadataResponse::Found(archive) = &*response else { panic!("Every URL distribution should have metadata: {version_id:?}") @@ -449,17 +466,28 @@ impl ResolutionGraph { } // Extract the hashes. - let hashes = Self::get_hashes(name, None, &version_id, version, preferences, index); + let hashes = Self::get_hashes( + name, + index, + None, + &version_id, + version, + preferences, + in_memory, + ); // Extract the metadata. let metadata = { - index.distributions().get(&version_id).and_then(|response| { - if let MetadataResponse::Found(archive) = &*response { - Some(archive.metadata.clone()) - } else { - None - } - }) + in_memory + .distributions() + .get(&version_id) + .and_then(|response| { + if let MetadataResponse::Found(archive) = &*response { + Some(archive.metadata.clone()) + } else { + None + } + }) }; (dist, hashes, metadata) @@ -470,11 +498,12 @@ impl ResolutionGraph { /// lockfile. fn get_hashes( name: &PackageName, + index: Option<&IndexUrl>, url: Option<&VerbatimParsedUrl>, version_id: &VersionId, version: &Version, preferences: &Preferences, - index: &InMemoryIndex, + in_memory: &InMemoryIndex, ) -> Vec { // 1. Look for hashes from the lockfile. if let Some(digests) = preferences.match_hashes(name, version) { @@ -484,7 +513,7 @@ impl ResolutionGraph { } // 2. Look for hashes for the distribution (i.e., the specific wheel or source distribution). - if let Some(metadata_response) = index.distributions().get(version_id) { + if let Some(metadata_response) = in_memory.distributions().get(version_id) { if let MetadataResponse::Found(ref archive) = *metadata_response { let mut digests = archive.hashes.clone(); digests.sort_unstable(); @@ -496,7 +525,13 @@ impl ResolutionGraph { // 3. Look for hashes from the registry, which are served at the package level. if url.is_none() { - if let Some(versions_response) = index.packages().get(name) { + let versions_response = if let Some(index) = index { + in_memory.explicit().get(&(name.clone(), index.clone())) + } else { + in_memory.implicit().get(name) + }; + + if let Some(versions_response) = versions_response { if let VersionsResponse::Found(ref version_maps) = *versions_response { if let Some(digests) = version_maps .iter() diff --git a/crates/uv-resolver/src/resolver/batch_prefetch.rs b/crates/uv-resolver/src/resolver/batch_prefetch.rs index 7cf9684fd4a7..586a5e3befba 100644 --- a/crates/uv-resolver/src/resolver/batch_prefetch.rs +++ b/crates/uv-resolver/src/resolver/batch_prefetch.rs @@ -6,13 +6,12 @@ use rustc_hash::FxHashMap; use tokio::sync::mpsc::Sender; use tracing::{debug, trace}; -use uv_distribution_types::{CompatibleDist, DistributionMetadata, IndexCapabilities}; -use uv_pep440::Version; - use crate::candidate_selector::CandidateSelector; use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner}; use crate::resolver::Request; use crate::{InMemoryIndex, PythonRequirement, ResolveError, ResolverMarkers, VersionsResponse}; +use uv_distribution_types::{CompatibleDist, DistributionMetadata, IndexCapabilities, IndexUrl}; +use uv_pep440::Version; enum BatchPrefetchStrategy { /// Go through the next versions assuming the existing selection and its constraints @@ -47,11 +46,12 @@ impl BatchPrefetcher { pub(crate) fn prefetch_batches( &mut self, next: &PubGrubPackage, + index: Option<&IndexUrl>, version: &Version, current_range: &Range, python_requirement: &PythonRequirement, request_sink: &Sender, - index: &InMemoryIndex, + in_memory: &InMemoryIndex, capabilities: &IndexCapabilities, selector: &CandidateSelector, markers: &ResolverMarkers, @@ -73,10 +73,17 @@ impl BatchPrefetcher { let total_prefetch = min(num_tried, 50); // This is immediate, we already fetched the version map. - let versions_response = index - .packages() - .wait_blocking(name) - .ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?; + let versions_response = if let Some(index) = index { + in_memory + .explicit() + .wait_blocking(&(name.clone(), index.clone())) + .ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))? + } else { + in_memory + .implicit() + .wait_blocking(name) + .ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))? + }; let VersionsResponse::Found(ref version_map) = *versions_response else { return Ok(()); @@ -191,7 +198,7 @@ impl BatchPrefetcher { ); prefetch_count += 1; - if index.distributions().register(candidate.version_id()) { + if in_memory.distributions().register(candidate.version_id()) { let request = Request::from(dist); request_sink.blocking_send(request)?; } diff --git a/crates/uv-resolver/src/resolver/fork_map.rs b/crates/uv-resolver/src/resolver/fork_map.rs index 47003267dd0d..fa9fd166696a 100644 --- a/crates/uv-resolver/src/resolver/fork_map.rs +++ b/crates/uv-resolver/src/resolver/fork_map.rs @@ -44,6 +44,11 @@ impl ForkMap { !self.get(package_name, markers).is_empty() } + /// Returns `true` if the map contains any values for a package. + pub(crate) fn contains_key(&self, package_name: &PackageName) -> bool { + self.0.contains_key(package_name) + } + /// Returns a list of values associated with a package that are compatible with the given fork. /// /// Compatibility implies that the markers on the requirement that contained this value diff --git a/crates/uv-resolver/src/resolver/index.rs b/crates/uv-resolver/src/resolver/index.rs index 9796c7769f13..222a9370876f 100644 --- a/crates/uv-resolver/src/resolver/index.rs +++ b/crates/uv-resolver/src/resolver/index.rs @@ -2,7 +2,7 @@ use std::hash::BuildHasherDefault; use std::sync::Arc; use rustc_hash::FxHasher; -use uv_distribution_types::VersionId; +use uv_distribution_types::{IndexUrl, VersionId}; use uv_normalize::PackageName; use uv_once_map::OnceMap; @@ -16,7 +16,9 @@ pub struct InMemoryIndex(Arc); struct SharedInMemoryIndex { /// A map from package name to the metadata for that package and the index where the metadata /// came from. - packages: FxOnceMap>, + implicit: FxOnceMap>, + + explicit: FxOnceMap<(PackageName, IndexUrl), Arc>, /// A map from package ID to metadata for that distribution. distributions: FxOnceMap>, @@ -26,8 +28,13 @@ pub(crate) type FxOnceMap = OnceMap>; impl InMemoryIndex { /// Returns a reference to the package metadata map. - pub fn packages(&self) -> &FxOnceMap> { - &self.0.packages + pub fn implicit(&self) -> &FxOnceMap> { + &self.0.implicit + } + + /// Returns a reference to the package metadata map. + pub fn explicit(&self) -> &FxOnceMap<(PackageName, IndexUrl), Arc> { + &self.0.explicit } /// Returns a reference to the distribution metadata map. diff --git a/crates/uv-resolver/src/resolver/indexes.rs b/crates/uv-resolver/src/resolver/indexes.rs index d925cd786274..556a4f444a58 100644 --- a/crates/uv-resolver/src/resolver/indexes.rs +++ b/crates/uv-resolver/src/resolver/indexes.rs @@ -1,12 +1,11 @@ -use crate::{DependencyMode, Manifest, ResolveError, ResolverMarkers}; - -use rustc_hash::FxHashMap; -use std::collections::hash_map::Entry; use uv_distribution_types::IndexUrl; use uv_normalize::PackageName; use uv_pep508::VerbatimUrl; use uv_pypi_types::RequirementSource; +use crate::resolver::ForkMap; +use crate::{DependencyMode, Manifest, ResolverMarkers}; + /// A map of package names to their explicit index. /// /// For example, given: @@ -21,7 +20,7 @@ use uv_pypi_types::RequirementSource; /// /// [`Indexes`] would contain a single entry mapping `torch` to `https://download.pytorch.org/whl/cu121`. #[derive(Debug, Default, Clone)] -pub(crate) struct Indexes(FxHashMap); +pub(crate) struct Indexes(ForkMap); impl Indexes { /// Determine the set of explicit, pinned indexes in the [`Manifest`]. @@ -29,8 +28,8 @@ impl Indexes { manifest: &Manifest, markers: &ResolverMarkers, dependencies: DependencyMode, - ) -> Result { - let mut indexes = FxHashMap::::default(); + ) -> Self { + let mut indexes = ForkMap::default(); for requirement in manifest.requirements(markers, dependencies) { let RequirementSource::Registry { @@ -40,28 +39,23 @@ impl Indexes { continue; }; let index = IndexUrl::from(VerbatimUrl::from_url(index.clone())); - match indexes.entry(requirement.name.clone()) { - Entry::Occupied(entry) => { - let existing = entry.get(); - if *existing != index { - return Err(ResolveError::ConflictingIndexes( - requirement.name.clone(), - existing.to_string(), - index.to_string(), - )); - } - } - Entry::Vacant(entry) => { - entry.insert(index); - } - } + indexes.add(&requirement, index); } - Ok(Self(indexes)) + Self(indexes) } - /// Return the explicit index for a given [`PackageName`]. - pub(crate) fn get(&self, package_name: &PackageName) -> Option<&IndexUrl> { - self.0.get(package_name) + /// Returns `true` if the map contains any indexes for a package. + pub(crate) fn contains_key(&self, name: &PackageName) -> bool { + self.0.contains_key(name) + } + + /// Return the explicit index used for a package in the given fork. + pub(crate) fn get( + &self, + package_name: &PackageName, + markers: &ResolverMarkers, + ) -> Vec<&IndexUrl> { + self.0.get(package_name, markers) } } diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs index 1bc06f5c5668..dea365b61251 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -44,6 +44,7 @@ use uv_warnings::warn_user_once; use crate::candidate_selector::{CandidateDist, CandidateSelector}; use crate::dependency_provider::UvDependencyProvider; use crate::error::{NoSolutionError, ResolveError}; +use crate::fork_indexes::ForkIndexes; use crate::fork_urls::ForkUrls; use crate::manifest::Manifest; use crate::pins::FilePins; @@ -208,7 +209,7 @@ impl dependency_mode: options.dependency_mode, urls: Urls::from_manifest(&manifest, &markers, git, options.dependency_mode)?, locals: Locals::from_manifest(&manifest, &markers, options.dependency_mode), - indexes: Indexes::from_manifest(&manifest, &markers, options.dependency_mode)?, + indexes: Indexes::from_manifest(&manifest, &markers, options.dependency_mode), groups: Groups::from_manifest(&manifest, &markers), project: manifest.project, workspace_members: manifest.workspace_members, @@ -334,6 +335,7 @@ impl ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState ResolverState( packages: impl Iterator)>, urls: &Urls, + indexes: &Indexes, python_requirement: &PythonRequirement, request_sink: &Sender, ) -> Result<(), ResolveError> { @@ -802,6 +825,10 @@ impl ResolverState ResolverState, range: &Range, pins: &mut FilePins, preferences: &Preferences, @@ -849,6 +877,7 @@ impl ResolverState ResolverState, range: &Range, package: &PubGrubPackage, preferences: &Preferences, @@ -974,11 +1004,17 @@ impl ResolverState, ) -> Result, ResolveError> { // Wait for the metadata to be available. - let versions_response = self - .index - .packages() - .wait_blocking(name) - .ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?; + let versions_response = if let Some(index) = index { + self.index + .explicit() + .wait_blocking(&(name.clone(), index.clone())) + .ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))? + } else { + self.index + .implicit() + .wait_blocking(name) + .ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))? + }; visited.insert(name.clone()); let version_maps = match *versions_response { @@ -1654,11 +1690,15 @@ impl ResolverState { - trace!("Received package metadata for: {package_name}"); - self.index - .packages() - .done(package_name, Arc::new(version_map)); + Some(Response::Package(name, index, version_map)) => { + trace!("Received package metadata for: {name}"); + if let Some(index) = index { + self.index + .explicit() + .done((name, index), Arc::new(version_map)); + } else { + self.index.implicit().done(name, Arc::new(version_map)); + } } Some(Response::Installed { dist, metadata }) => { trace!("Received installed distribution metadata for: {dist}"); @@ -1727,7 +1767,11 @@ impl ResolverState ResolverState ResolverState, fork_urls: ForkUrls, + fork_indexes: &ForkIndexes, markers: ResolverMarkers, visited: &FxHashSet, index_locations: &IndexLocations, @@ -1965,7 +2010,12 @@ impl ResolverState, version: &Version, urls: &Urls, + indexes: &Indexes, locals: &Locals, mut dependencies: Vec, git: &GitResolver, @@ -2170,6 +2227,11 @@ impl ForkState { *version = version.union(&local); } } + + // If the package is pinned to an exact index, add it to the fork. + for index in indexes.get(name, &self.markers) { + self.fork_indexes.insert(name, index, &self.markers)?; + } } if let Some(for_package) = for_package { @@ -2319,6 +2381,9 @@ impl ForkState { _ => continue, }; let self_url = self_name.as_ref().and_then(|name| self.fork_urls.get(name)); + let self_index = self_name + .as_ref() + .and_then(|name| self.fork_indexes.get(name)); match **dependency_package { PubGrubPackageInner::Package { @@ -2331,15 +2396,18 @@ impl ForkState { continue; } let to_url = self.fork_urls.get(dependency_name); + let to_index = self.fork_indexes.get(dependency_name); let edge = ResolutionDependencyEdge { from: self_name.cloned(), from_version: self_version.clone(), from_url: self_url.cloned(), + from_index: self_index.cloned(), from_extra: self_extra.cloned(), from_dev: self_dev.cloned(), to: dependency_name.clone(), to_version: dependency_version.clone(), to_url: to_url.cloned(), + to_index: to_index.cloned(), to_extra: dependency_extra.clone(), to_dev: dependency_dev.clone(), marker: MarkerTree::TRUE, @@ -2356,15 +2424,18 @@ impl ForkState { continue; } let to_url = self.fork_urls.get(dependency_name); + let to_index = self.fork_indexes.get(dependency_name); let edge = ResolutionDependencyEdge { from: self_name.cloned(), from_version: self_version.clone(), from_url: self_url.cloned(), + from_index: self_index.cloned(), from_extra: self_extra.cloned(), from_dev: self_dev.cloned(), to: dependency_name.clone(), to_version: dependency_version.clone(), to_url: to_url.cloned(), + to_index: to_index.cloned(), to_extra: None, to_dev: None, marker: dependency_marker.clone(), @@ -2382,15 +2453,18 @@ impl ForkState { continue; } let to_url = self.fork_urls.get(dependency_name); + let to_index = self.fork_indexes.get(dependency_name); let edge = ResolutionDependencyEdge { from: self_name.cloned(), from_version: self_version.clone(), from_url: self_url.cloned(), + from_index: self_index.cloned(), from_extra: self_extra.cloned(), from_dev: self_dev.cloned(), to: dependency_name.clone(), to_version: dependency_version.clone(), to_url: to_url.cloned(), + to_index: to_index.cloned(), to_extra: Some(dependency_extra.clone()), to_dev: None, marker: MarkerTree::from(dependency_marker.clone()), @@ -2408,15 +2482,18 @@ impl ForkState { continue; } let to_url = self.fork_urls.get(dependency_name); + let to_index = self.fork_indexes.get(dependency_name); let edge = ResolutionDependencyEdge { from: self_name.cloned(), from_version: self_version.clone(), from_url: self_url.cloned(), + from_index: self_index.cloned(), from_extra: self_extra.cloned(), from_dev: self_dev.cloned(), to: dependency_name.clone(), to_version: dependency_version.clone(), to_url: to_url.cloned(), + to_index: to_index.cloned(), to_extra: None, to_dev: Some(dependency_dev.clone()), marker: MarkerTree::from(dependency_marker.clone()), @@ -2445,6 +2522,7 @@ impl ForkState { extra: extra.clone(), dev: dev.clone(), url: self.fork_urls.get(name).cloned(), + index: self.fork_indexes.get(name).cloned(), }, version, )) @@ -2485,6 +2563,9 @@ pub(crate) struct ResolutionPackage { pub(crate) dev: Option, /// For index packages, this is `None`. pub(crate) url: Option, + /// For URL packages, this is `None`, and is only `Some` for packages that are pinned to a + /// specific index via `tool.uv.sources`. + pub(crate) index: Option, } /// The `from_` fields and the `to_` fields allow mapping to the originating and target @@ -2495,11 +2576,13 @@ pub(crate) struct ResolutionDependencyEdge { pub(crate) from: Option, pub(crate) from_version: Version, pub(crate) from_url: Option, + pub(crate) from_index: Option, pub(crate) from_extra: Option, pub(crate) from_dev: Option, pub(crate) to: PackageName, pub(crate) to_version: Version, pub(crate) to_url: Option, + pub(crate) to_index: Option, pub(crate) to_extra: Option, pub(crate) to_dev: Option, pub(crate) marker: MarkerTree, @@ -2585,7 +2668,7 @@ impl Display for Request { #[allow(clippy::large_enum_variant)] enum Response { /// The returned metadata for a package hosted on a registry. - Package(PackageName, VersionsResponse), + Package(PackageName, Option, VersionsResponse), /// The returned metadata for a distribution. Dist { dist: Dist, diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 95bbe4ac1c27..bf551b299d22 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -548,16 +548,6 @@ impl TryFrom for Sources { return Err(SourceError::EmptySources); } - // Ensure that there is at most one registry source. - if sources - .iter() - .filter(|source| matches!(source, Source::Registry { .. })) - .nth(1) - .is_some() - { - return Err(SourceError::MultipleIndexes); - } - Ok(Self(sources)) } } @@ -967,8 +957,6 @@ pub enum SourceError { OverlappingMarkers(String, String, String), #[error("Must provide at least one source")] EmptySources, - #[error("Sources can only include a single index source")] - MultipleIndexes, } impl Source { diff --git a/crates/uv/tests/it/lock.rs b/crates/uv/tests/it/lock.rs index 01f6f290e49b..7f1a57fc223d 100644 --- a/crates/uv/tests/it/lock.rs +++ b/crates/uv/tests/it/lock.rs @@ -7352,8 +7352,8 @@ fn lock_warn_missing_transitive_lower_bounds() -> Result<()> { ----- stderr ----- Resolved 6 packages in [TIME] - warning: The transitive dependency `packaging` is unpinned. Consider setting a lower bound with a constraint when using `--resolution-strategy lowest` to avoid using outdated versions. warning: The transitive dependency `colorama` is unpinned. Consider setting a lower bound with a constraint when using `--resolution-strategy lowest` to avoid using outdated versions. + warning: The transitive dependency `packaging` is unpinned. Consider setting a lower bound with a constraint when using `--resolution-strategy lowest` to avoid using outdated versions. warning: The transitive dependency `iniconfig` is unpinned. Consider setting a lower bound with a constraint when using `--resolution-strategy lowest` to avoid using outdated versions. "###); @@ -12431,7 +12431,7 @@ fn lock_repeat_named_index_cli() -> Result<()> { )?; // Resolve to the PyTorch index. - uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER"), @r###" + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" success: true exit_code: 0 ----- stdout ----- @@ -12490,7 +12490,7 @@ fn lock_repeat_named_index_cli() -> Result<()> { // Resolve to PyPI, since the PyTorch index is replaced by the Packse index, which doesn't // include `jinja2`. - uv_snapshot!(context.filters(), context.lock().arg("--index").arg(format!("pytorch={}", packse_index_url())).env_remove("UV_EXCLUDE_NEWER"), @r###" + uv_snapshot!(context.filters(), context.lock().arg("--index").arg(format!("pytorch={}", packse_index_url())).env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" success: true exit_code: 0 ----- stdout ----- @@ -14625,7 +14625,6 @@ fn lock_multiple_sources_conflict() -> Result<()> { Ok(()) } -/// Multiple `index` entries is not yet supported. #[test] fn lock_multiple_sources_index() -> Result<()> { let context = TestContext::new("3.12"); @@ -14637,29 +14636,411 @@ fn lock_multiple_sources_index() -> Result<()> { name = "project" version = "0.1.0" requires-python = ">=3.12" - dependencies = ["iniconfig"] + dependencies = ["jinja2>=3"] [tool.uv.sources] - iniconfig = [ - { index = "pytorch", marker = "sys_platform != 'win32'" }, - { index = "internal", marker = "sys_platform == 'win32'" }, + jinja2 = [ + { index = "torch-cu118", marker = "sys_platform == 'win32'"}, + { index = "torch-cu124", marker = "sys_platform != 'win32'"}, ] + + [[tool.uv.index]] + name = "torch-cu118" + url = "https://download.pytorch.org/whl/cu118" + + [[tool.uv.index]] + name = "torch-cu124" + url = "https://download.pytorch.org/whl/cu124" "#, )?; - uv_snapshot!(context.filters(), context.lock(), @r###" + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 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" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform != 'win32'", + ] + + [[package]] + name = "jinja2" + version = "3.1.3" + source = { registry = "https://download.pytorch.org/whl/cu118" } + dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'win32'" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + ] + + [[package]] + name = "jinja2" + version = "3.1.3" + source = { registry = "https://download.pytorch.org/whl/cu124" } + dependencies = [ + { name = "markupsafe", marker = "sys_platform != 'win32'" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + ] + + [[package]] + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://download.pytorch.org/whl/cu118" } + wheels = [ + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" }, + { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu124" }, marker = "sys_platform != 'win32'" }, + ] + + [package.metadata] + requires-dist = [ + { name = "jinja2", marker = "sys_platform != 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu124" }, + { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + Ok(()) +} + +#[test] +fn lock_multiple_sources_index_mixed() -> 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 = ["jinja2>=3"] + + [tool.uv.sources] + jinja2 = [ + { index = "torch-cu118", marker = "sys_platform == 'win32'"}, + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", marker = "sys_platform != 'win32'"}, + ] + + [[tool.uv.index]] + name = "torch-cu118" + url = "https://download.pytorch.org/whl/cu118" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 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" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform != 'win32'", + ] + + [[package]] + name = "jinja2" + version = "3.1.3" + source = { registry = "https://download.pytorch.org/whl/cu118" } + resolution-markers = [ + "sys_platform == 'win32'", + ] + dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'win32'" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + ] + + [[package]] + name = "jinja2" + version = "3.1.4" + source = { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" } + resolution-markers = [ + "sys_platform != 'win32'", + ] + dependencies = [ + { name = "markupsafe", marker = "sys_platform != 'win32'" }, + ] + wheels = [ + { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl", hash = "sha256:bc5dd2abb727a5319567b7a813e6a2e7318c39f4f487cfe6c89c6f9c7d25197d" }, + ] + + [package.metadata] + requires-dist = [ + { name = "babel", marker = "extra == 'i18n'", specifier = ">=2.7" }, + { name = "markupsafe", specifier = ">=2.0" }, + ] + + [[package]] + name = "markupsafe" + version = "2.1.5" + source = { registry = "https://download.pytorch.org/whl/cu118" } + wheels = [ + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:8dec4936e9c3100156f8a2dc89c4b88d5c435175ff03413b443469c7c8c5f4d1" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:3c6b973f22eb18a789b1460b4b91bf04ae3f0c4234a0a6aa6b0a92f6f7b951d4" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ac07bad82163452a6884fe8fa0963fb98c2346ba78d779ec06bd7a6262132aee" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f5dfb42c4604dddc8e4305050aa6deb084540643ed5804d7455b5df8fe16f5e5" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ea3d8a3d18833cf4304cd2fc9cbb1efe188ca9b5efef2bdac7adc20594a0e46b" }, + { url = "https://download.pytorch.org/whl/MarkupSafe-2.1.5-cp312-cp312-win_amd64.whl", hash = "sha256:823b65d8706e32ad2df51ed89496147a42a2a6e01c13cfb6ffb8b1e92bc910bb" }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" }, + { name = "jinja2", version = "3.1.4", source = { url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" }, marker = "sys_platform != 'win32'" }, + ] + + [package.metadata] + requires-dist = [ + { name = "jinja2", marker = "sys_platform != 'win32'", url = "https://files.pythonhosted.org/packages/31/80/3a54838c3fb461f6fec263ebf3a3a41771bd05190238de3486aae8540c36/jinja2-3.1.4-py3-none-any.whl" }, + { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] + "###); + + Ok(()) +} + +#[test] +fn lock_multiple_sources_index_non_total() -> 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 = ["jinja2>=3"] + + [tool.uv.sources] + jinja2 = [ + { index = "torch-cu118", marker = "sys_platform == 'win32'"}, + ] + + [[tool.uv.index]] + name = "torch-cu118" + url = "https://download.pytorch.org/whl/cu118" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" success: false exit_code: 2 ----- stdout ----- ----- stderr ----- - error: Failed to parse: `pyproject.toml` - Caused by: TOML parse error at line 9, column 21 - | - 9 | iniconfig = [ - | ^ - Sources can only include a single index source + Resolved 4 packages in [TIME] + error: Found duplicate package `jinja2==3.1.3 @ registry+https://download.pytorch.org/whl/cu118` + "###); + + Ok(()) +} + +#[test] +fn lock_multiple_sources_index_explicit() -> 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 = ["jinja2>=3"] + + [tool.uv.sources] + jinja2 = [ + { index = "torch-cu118", marker = "sys_platform == 'win32'"}, + ] + + [[tool.uv.index]] + name = "torch-cu118" + url = "https://download.pytorch.org/whl/cu118" + explicit = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock().env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 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" + resolution-markers = [ + "sys_platform == 'win32'", + "sys_platform != 'win32'", + ] + + [[package]] + name = "jinja2" + version = "3.1.3" + source = { registry = "https://download.pytorch.org/whl/cu118" } + dependencies = [ + { name = "markupsafe", marker = "sys_platform == 'win32'" }, + ] + wheels = [ + { url = "https://download.pytorch.org/whl/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa" }, + ] + + [[package]] + name = "jinja2" + version = "3.1.3" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "markupsafe", marker = "sys_platform != 'win32'" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/b2/5e/3a21abf3cd467d7876045335e681d276ac32492febe6d98ad89562d1a7e1/Jinja2-3.1.3.tar.gz", hash = "sha256:ac8bd6544d4bb2c9792bf3a159e80bba8fda7f07e81bc3aed565432d5925ba90", size = 268261 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/30/6d/6de6be2d02603ab56e72997708809e8a5b0fbfee080735109b40a3564843/Jinja2-3.1.3-py3-none-any.whl", hash = "sha256:7d6d50dd97d52cbc355597bd845fabfbac3f551e1f99619e39a35ce8c370b5fa", size = 133236 }, + ] + + [[package]] + name = "markupsafe" + version = "3.0.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/b4/d2/38ff920762f2247c3af5cbbbbc40756f575d9692d381d7c520f45deb9b8f/markupsafe-3.0.1.tar.gz", hash = "sha256:3e683ee4f5d0fa2dde4db77ed8dd8a876686e3fc417655c2ece9a90576905344", size = 20249 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/45/6d/72ed58d42a12bd9fc288dbff6dd8d03ea973a232ac0538d7f88d105b5251/MarkupSafe-3.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:8ae369e84466aa70f3154ee23c1451fda10a8ee1b63923ce76667e3077f2b0c4", size = 14322 }, + { url = "https://files.pythonhosted.org/packages/86/f5/241238f89cdd6461ac9f521af8389f9a48fab97e4f315c69e9e0d52bc919/MarkupSafe-3.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:40f1e10d51c92859765522cbd79c5c8989f40f0419614bcdc5015e7b6bf97fc5", size = 12380 }, + { url = "https://files.pythonhosted.org/packages/27/94/79751928bca5841416d8ca02e22198672e021d5c7120338e2a6e3771f8fc/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5a4cb365cb49b750bdb60b846b0c0bc49ed62e59a76635095a179d440540c346", size = 24099 }, + { url = "https://files.pythonhosted.org/packages/10/6e/1b8070bbfc467429c7983cd5ffd4ec57e1d501763d974c7caaa0a9a79f4c/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ee3941769bd2522fe39222206f6dd97ae83c442a94c90f2b7a25d847d40f4729", size = 23249 }, + { url = "https://files.pythonhosted.org/packages/66/50/9389ae6cdff78d7481a2a2641830b5eb1d1f62177550e73355a810a889c9/MarkupSafe-3.0.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:62fada2c942702ef8952754abfc1a9f7658a4d5460fabe95ac7ec2cbe0d02abc", size = 23149 }, + { url = "https://files.pythonhosted.org/packages/16/02/5dddff5366fde47133186efb847fa88bddef85914bbe623e25cfeccb3517/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:4c2d64fdba74ad16138300815cfdc6ab2f4647e23ced81f59e940d7d4a1469d9", size = 23864 }, + { url = "https://files.pythonhosted.org/packages/f3/f1/700ee6655561cfda986e03f7afc309e3738918551afa7dedd99225586227/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:fb532dd9900381d2e8f48172ddc5a59db4c445a11b9fab40b3b786da40d3b56b", size = 23440 }, + { url = "https://files.pythonhosted.org/packages/fb/3e/d26623ac7f16709823b4c80e0b4a1c9196eeb46182a6c1d47b5e0c8434f4/MarkupSafe-3.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0f84af7e813784feb4d5e4ff7db633aba6c8ca64a833f61d8e4eade234ef0c38", size = 23610 }, + { url = "https://files.pythonhosted.org/packages/51/04/1f8da0810c39cb9fcff96b6baed62272c97065e9cf11471965a161439e20/MarkupSafe-3.0.1-cp312-cp312-win32.whl", hash = "sha256:cbf445eb5628981a80f54087f9acdbf84f9b7d862756110d172993b9a5ae81aa", size = 15113 }, + { url = "https://files.pythonhosted.org/packages/eb/24/a36dc37365bdd358b1e583cc40475593e36ab02cb7da6b3d0b9c05b0da7a/MarkupSafe-3.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:a10860e00ded1dd0a65b83e717af28845bb7bd16d8ace40fe5531491de76b79f", size = 15611 }, + { url = "https://files.pythonhosted.org/packages/b1/60/4572a8aa1beccbc24b133aa0670781a5d2697f4fa3fecf0a87b46383174b/MarkupSafe-3.0.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e81c52638315ff4ac1b533d427f50bc0afc746deb949210bc85f05d4f15fd772", size = 14325 }, + { url = "https://files.pythonhosted.org/packages/38/42/849915b99a765ec104bfd07ee933de5fc9c58fa9570efa7db81717f495d8/MarkupSafe-3.0.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:312387403cd40699ab91d50735ea7a507b788091c416dd007eac54434aee51da", size = 12373 }, + { url = "https://files.pythonhosted.org/packages/ef/82/4caaebd963c6d60b28e4445f38841d24f8b49bc10594a09956c9d73bfc08/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2ae99f31f47d849758a687102afdd05bd3d3ff7dbab0a8f1587981b58a76152a", size = 24059 }, + { url = "https://files.pythonhosted.org/packages/20/15/6b319be2f79fcfa3173f479d69f4e950b5c9b642db4f22cf73ae5ade745f/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c97ff7fedf56d86bae92fa0a646ce1a0ec7509a7578e1ed238731ba13aabcd1c", size = 23211 }, + { url = "https://files.pythonhosted.org/packages/9d/3f/8963bdf4962feb2154475acb7dc350f04217b5e0be7763a39b432291e229/MarkupSafe-3.0.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:a7420ceda262dbb4b8d839a4ec63d61c261e4e77677ed7c66c99f4e7cb5030dd", size = 23095 }, + { url = "https://files.pythonhosted.org/packages/af/93/f770bc70953d32de0c6ce4bcb76271512123a1ead91aaef625a020c5bfaf/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:45d42d132cff577c92bfba536aefcfea7e26efb975bd455db4e6602f5c9f45e7", size = 23901 }, + { url = "https://files.pythonhosted.org/packages/11/92/1e5a33aa0a1190161238628fb68eb1bc5e67b56a5c89f0636328704b463a/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:4c8817557d0de9349109acb38b9dd570b03cc5014e8aabf1cbddc6e81005becd", size = 23463 }, + { url = "https://files.pythonhosted.org/packages/0d/fe/657efdfe385d2a3a701f2c4fcc9577c63c438aeefdd642d0d956c4ecd225/MarkupSafe-3.0.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6a54c43d3ec4cf2a39f4387ad044221c66a376e58c0d0e971d47c475ba79c6b5", size = 23569 }, + { url = "https://files.pythonhosted.org/packages/cf/24/587dea40304046ace60f846cedaebc0d33d967a3ce46c11395a10e7a78ba/MarkupSafe-3.0.1-cp313-cp313-win32.whl", hash = "sha256:c91b394f7601438ff79a4b93d16be92f216adb57d813a78be4446fe0f6bc2d8c", size = 15117 }, + { url = "https://files.pythonhosted.org/packages/32/8f/d8961d633f26a011b4fe054f3bfff52f673423b8c431553268741dfb089e/MarkupSafe-3.0.1-cp313-cp313-win_amd64.whl", hash = "sha256:fe32482b37b4b00c7a52a07211b479653b7fe4f22b2e481b9a9b099d8a430f2f", size = 15613 }, + { url = "https://files.pythonhosted.org/packages/9e/93/d6367ffbcd0c5c371370767f768eaa32af60bc411245b8517e383c6a2b12/MarkupSafe-3.0.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:17b2aea42a7280db02ac644db1d634ad47dcc96faf38ab304fe26ba2680d359a", size = 14563 }, + { url = "https://files.pythonhosted.org/packages/4a/37/f813c3835747dec08fe19ac9b9eced01fdf93a4b3e626521675dc7f423a9/MarkupSafe-3.0.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:852dc840f6d7c985603e60b5deaae1d89c56cb038b577f6b5b8c808c97580f1d", size = 12505 }, + { url = "https://files.pythonhosted.org/packages/72/bf/800b4d1580298ca91ccd6c95915bbd147142dad1b8cf91d57b93b28670dd/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0778de17cff1acaeccc3ff30cd99a3fd5c50fc58ad3d6c0e0c4c58092b859396", size = 25358 }, + { url = "https://files.pythonhosted.org/packages/fd/78/26e209abc8f0a379f031f0acc151231974e5b153d7eda5759d17d8f329f2/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:800100d45176652ded796134277ecb13640c1a537cad3b8b53da45aa96330453", size = 23797 }, + { url = "https://files.pythonhosted.org/packages/09/e1/918496a9390891756efee818880e71c1bbaf587f4dc8ede3f3852357310a/MarkupSafe-3.0.1-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d06b24c686a34c86c8c1fba923181eae6b10565e4d80bdd7bc1c8e2f11247aa4", size = 23743 }, + { url = "https://files.pythonhosted.org/packages/cd/c6/26f576cd58d6c2decd9045e4e3f3c5dbc01ea6cb710916e7bbb6ebd95b6b/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:33d1c36b90e570ba7785dacd1faaf091203d9942bc036118fab8110a401eb1a8", size = 25076 }, + { url = "https://files.pythonhosted.org/packages/b5/fa/10b24fb3b0e15fe5389dc88ecc6226ede08297e0ba7130610efbe0cdfb27/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:beeebf760a9c1f4c07ef6a53465e8cfa776ea6a2021eda0d0417ec41043fe984", size = 24037 }, + { url = "https://files.pythonhosted.org/packages/c8/81/4b3f5537d9f6cc4f5c80d6c4b78af9a5247fd37b5aba95807b2cbc336b9a/MarkupSafe-3.0.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:bbde71a705f8e9e4c3e9e33db69341d040c827c7afa6789b14c6e16776074f5a", size = 24015 }, + { url = "https://files.pythonhosted.org/packages/5f/07/8e8dcecd53216c5e01a51e84c32a2bce166690ed19c184774b38cd41921d/MarkupSafe-3.0.1-cp313-cp313t-win32.whl", hash = "sha256:82b5dba6eb1bcc29cc305a18a3c5365d2af06ee71b123216416f7e20d2a84e5b", size = 15213 }, + { url = "https://files.pythonhosted.org/packages/0d/87/4c364e0f109eea2402079abecbe33fef4f347b551a11423d1f4e187ea497/MarkupSafe-3.0.1-cp313-cp313t-win_amd64.whl", hash = "sha256:730d86af59e0e43ce277bb83970530dd223bf7f2a838e086b50affa6ec5f9295", size = 15741 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "jinja2", version = "3.1.3", source = { registry = "https://download.pytorch.org/whl/cu118" }, marker = "sys_platform == 'win32'" }, + { name = "jinja2", version = "3.1.3", source = { registry = "https://pypi.org/simple" }, marker = "sys_platform != 'win32'" }, + ] + + [package.metadata] + requires-dist = [ + { name = "jinja2", marker = "sys_platform != 'win32'", specifier = ">=3" }, + { name = "jinja2", marker = "sys_platform == 'win32'", specifier = ">=3", index = "https://download.pytorch.org/whl/cu118" }, + ] + "### + ); + }); + + // Re-run with `--locked`. + uv_snapshot!(context.filters(), context.lock().arg("--locked").env_remove(EnvVars::UV_EXCLUDE_NEWER), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 4 packages in [TIME] "###); Ok(()) diff --git a/docs/concepts/dependencies.md b/docs/concepts/dependencies.md index e8fa89388bc1..4c832758a199 100644 --- a/docs/concepts/dependencies.md +++ b/docs/concepts/dependencies.md @@ -272,7 +272,9 @@ download the source from GitHub on macOS, and fall back to PyPI on all other pla You can specify multiple sources for a single dependency by providing a list of sources, disambiguated by [PEP 508](https://peps.python.org/pep-0508/#environment-markers)-compatible -environment markers. For example, to pull in different `httpx` commits on macOS vs. Linux: +environment markers. + +For example, to pull in different `httpx` commits on macOS vs. Linux: ```toml title="pyproject.toml" [project] @@ -287,6 +289,29 @@ httpx = [ ] ``` +This strategy even extends to pulling packages from different indexes based on environment markers. +For example, to pull `torch` from different PyTorch indexes based on the platform: + +```toml title="pyproject.toml" +[project] +dependencies = ["torch"] + +[tool.uv.sources] +torch = [ + { index = "torch-cu118", marker = "sys_platform == 'darwin'"}, + { index = "torch-cu124", marker = "sys_platform != 'darwin'"}, +] + +[[tool.uv.index]] +name = "torch-cu118" +url = "https://download.pytorch.org/whl/cu118" + +[[tool.uv.index]] +name = "torch-cu124" +url = "https://download.pytorch.org/whl/cu124" + +``` + ## Optional dependencies It is common for projects that are published as libraries to make some features optional to reduce diff --git a/docs/configuration/indexes.md b/docs/configuration/indexes.md index 9bc0c7a85c97..a4eeefe02013 100644 --- a/docs/configuration/indexes.md +++ b/docs/configuration/indexes.md @@ -51,6 +51,28 @@ name = "pytorch" url = "https://download.pytorch.org/whl/cpu" ``` +Similarly, to pull from a different index based on the platform, you can provide a list of sources +disambiguated by environment markers: + +```toml title="pyproject.toml" +[project] +dependencies = ["pytorch"] + +[tool.uv.sources] +pytorch = [ + { index = "torch-cu118", marker = "sys_platform == 'darwin'"}, + { index = "torch-cu124", marker = "sys_platform != 'darwin'"}, +] + +[[tool.uv.index]] +name = "torch-cu118" +url = "https://download.pytorch.org/whl/cu118" + +[[tool.uv.index]] +name = "torch-cu124" +url = "https://download.pytorch.org/whl/cu124" +``` + An index can be marked as `explicit = true` to prevent packages from being installed from that index unless explicitly pinned to it. For example, to ensure that `torch` is installed from the `pytorch` index, but all other packages are installed from PyPI, add the following to your `pyproject.toml`: