Skip to content

Commit ae705cb

Browse files
committed
Allow multiple indexes
1 parent 1ba4caa commit ae705cb

File tree

12 files changed

+655
-105
lines changed

12 files changed

+655
-105
lines changed

crates/uv-resolver/src/error.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,16 @@ pub enum ResolveError {
5252
fork_markers: MarkerTree,
5353
},
5454

55+
#[error("Requirements contain conflicting indexes for package `{0}`:\n- {}", _1.join("\n- "))]
56+
ConflictingIndexesUniversal(PackageName, Vec<String>),
57+
58+
#[error("Requirements contain conflicting indexes for package `{package_name}` in split `{fork_markers:?}`:\n- {}", indexes.join("\n- "))]
59+
ConflictingIndexesFork {
60+
package_name: PackageName,
61+
indexes: Vec<String>,
62+
fork_markers: MarkerTree,
63+
},
64+
5565
#[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")]
5666
ConflictingIndexes(PackageName, String, String),
5767

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
use std::collections::hash_map::Entry;
2+
3+
use rustc_hash::FxHashMap;
4+
5+
use distribution_types::IndexUrl;
6+
use uv_normalize::PackageName;
7+
8+
use crate::resolver::ResolverMarkers;
9+
use crate::ResolveError;
10+
11+
/// See [`crate::resolver::ForkState`].
12+
#[derive(Default, Debug, Clone)]
13+
pub(crate) struct ForkIndexes(FxHashMap<PackageName, IndexUrl>);
14+
15+
impl ForkIndexes {
16+
/// Get the [`IndexUrl`] previously used for a package in this fork.
17+
pub(crate) fn get(&self, package_name: &PackageName) -> Option<&IndexUrl> {
18+
self.0.get(package_name)
19+
}
20+
21+
/// Check that this is the only [`IndexUrl`] used for this package in this fork.
22+
pub(crate) fn insert(
23+
&mut self,
24+
package_name: &PackageName,
25+
index: &IndexUrl,
26+
fork_markers: &ResolverMarkers,
27+
) -> Result<(), ResolveError> {
28+
match self.0.entry(package_name.clone()) {
29+
Entry::Occupied(previous) => {
30+
if previous.get() != index {
31+
let mut conflicts = vec![previous.get().to_string(), index.to_string()];
32+
conflicts.sort();
33+
return match fork_markers {
34+
ResolverMarkers::Universal { .. }
35+
| ResolverMarkers::SpecificEnvironment(_) => {
36+
Err(ResolveError::ConflictingIndexesUniversal(
37+
package_name.clone(),
38+
conflicts,
39+
))
40+
}
41+
ResolverMarkers::Fork(fork_markers) => {
42+
Err(ResolveError::ConflictingIndexesFork {
43+
package_name: package_name.clone(),
44+
indexes: conflicts,
45+
fork_markers: fork_markers.clone(),
46+
})
47+
}
48+
};
49+
}
50+
}
51+
Entry::Vacant(vacant) => {
52+
vacant.insert(index.clone());
53+
}
54+
}
55+
Ok(())
56+
}
57+
}

crates/uv-resolver/src/fork_urls.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ use uv_normalize::PackageName;
99
use crate::resolver::ResolverMarkers;
1010
use crate::ResolveError;
1111

12-
/// See [`crate::resolver::SolveState`].
12+
/// See [`crate::resolver::ForkState`].
1313
#[derive(Default, Debug, Clone)]
1414
pub(crate) struct ForkUrls(FxHashMap<PackageName, VerbatimParsedUrl>);
1515

crates/uv-resolver/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ mod error;
3434
mod exclude_newer;
3535
mod exclusions;
3636
mod flat_index;
37+
mod fork_indexes;
3738
mod fork_urls;
3839
mod graph_ops;
3940
mod lock;

crates/uv-resolver/src/resolution/graph.rs

Lines changed: 57 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ use std::collections::BTreeMap;
22
use std::fmt::{Display, Formatter};
33

44
use distribution_types::{
5-
Dist, DistributionMetadata, Name, ResolutionDiagnostic, ResolvedDist, VersionId,
5+
Dist, DistributionMetadata, IndexUrl, Name, ResolutionDiagnostic, ResolvedDist, VersionId,
66
VersionOrUrlRef,
77
};
88
use indexmap::IndexSet;
@@ -88,6 +88,7 @@ struct PackageRef<'a> {
8888
package_name: &'a PackageName,
8989
version: &'a Version,
9090
url: Option<&'a VerbatimParsedUrl>,
91+
index: Option<&'a IndexUrl>,
9192
extra: Option<&'a ExtraName>,
9293
group: Option<&'a GroupName>,
9394
}
@@ -284,6 +285,7 @@ impl ResolutionGraph {
284285
package_name: from,
285286
version: &edge.from_version,
286287
url: edge.from_url.as_ref(),
288+
index: edge.from_index.as_ref(),
287289
extra: edge.from_extra.as_ref(),
288290
group: edge.from_dev.as_ref(),
289291
}]
@@ -292,6 +294,7 @@ impl ResolutionGraph {
292294
package_name: &edge.to,
293295
version: &edge.to_version,
294296
url: edge.to_url.as_ref(),
297+
index: edge.to_index.as_ref(),
295298
extra: edge.to_extra.as_ref(),
296299
group: edge.to_dev.as_ref(),
297300
}];
@@ -320,7 +323,7 @@ impl ResolutionGraph {
320323
diagnostics: &mut Vec<ResolutionDiagnostic>,
321324
preferences: &Preferences,
322325
pins: &FilePins,
323-
index: &InMemoryIndex,
326+
in_memory: &InMemoryIndex,
324327
git: &GitResolver,
325328
package: &'a ResolutionPackage,
326329
version: &'a Version,
@@ -330,16 +333,18 @@ impl ResolutionGraph {
330333
extra,
331334
dev,
332335
url,
336+
index,
333337
} = &package;
334338
// Map the package to a distribution.
335339
let (dist, hashes, metadata) = Self::parse_dist(
336340
name,
341+
index.as_ref(),
337342
url.as_ref(),
338343
version,
339344
pins,
340345
diagnostics,
341346
preferences,
342-
index,
347+
in_memory,
343348
git,
344349
)?;
345350

@@ -366,7 +371,7 @@ impl ResolutionGraph {
366371
}
367372

368373
// Add the distribution to the graph.
369-
let index = petgraph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
374+
let node = petgraph.add_node(ResolutionGraphNode::Dist(AnnotatedDist {
370375
dist,
371376
name: name.clone(),
372377
version: version.clone(),
@@ -381,22 +386,24 @@ impl ResolutionGraph {
381386
package_name: name,
382387
version,
383388
url: url.as_ref(),
389+
index: index.as_ref(),
384390
extra: extra.as_ref(),
385391
group: dev.as_ref(),
386392
},
387-
index,
393+
node,
388394
);
389395
Ok(())
390396
}
391397

392398
fn parse_dist(
393399
name: &PackageName,
400+
index: Option<&IndexUrl>,
394401
url: Option<&VerbatimParsedUrl>,
395402
version: &Version,
396403
pins: &FilePins,
397404
diagnostics: &mut Vec<ResolutionDiagnostic>,
398405
preferences: &Preferences,
399-
index: &InMemoryIndex,
406+
in_memory: &InMemoryIndex,
400407
git: &GitResolver,
401408
) -> Result<(ResolvedDist, Vec<HashDigest>, Option<Metadata>), ResolveError> {
402409
Ok(if let Some(url) = url {
@@ -406,14 +413,24 @@ impl ResolutionGraph {
406413
let version_id = VersionId::from_url(&url.verbatim);
407414

408415
// Extract the hashes.
409-
let hashes =
410-
Self::get_hashes(name, Some(url), &version_id, version, preferences, index);
416+
let hashes = Self::get_hashes(
417+
name,
418+
index,
419+
Some(url),
420+
&version_id,
421+
version,
422+
preferences,
423+
in_memory,
424+
);
411425

412426
// Extract the metadata.
413427
let metadata = {
414-
let response = index.distributions().get(&version_id).unwrap_or_else(|| {
415-
panic!("Every URL distribution should have metadata: {version_id:?}")
416-
});
428+
let response = in_memory
429+
.distributions()
430+
.get(&version_id)
431+
.unwrap_or_else(|| {
432+
panic!("Every URL distribution should have metadata: {version_id:?}")
433+
});
417434

418435
let MetadataResponse::Found(archive) = &*response else {
419436
panic!("Every URL distribution should have metadata: {version_id:?}")
@@ -449,17 +466,28 @@ impl ResolutionGraph {
449466
}
450467

451468
// Extract the hashes.
452-
let hashes = Self::get_hashes(name, None, &version_id, version, preferences, index);
469+
let hashes = Self::get_hashes(
470+
name,
471+
index,
472+
None,
473+
&version_id,
474+
version,
475+
preferences,
476+
in_memory,
477+
);
453478

454479
// Extract the metadata.
455480
let metadata = {
456-
index.distributions().get(&version_id).and_then(|response| {
457-
if let MetadataResponse::Found(archive) = &*response {
458-
Some(archive.metadata.clone())
459-
} else {
460-
None
461-
}
462-
})
481+
in_memory
482+
.distributions()
483+
.get(&version_id)
484+
.and_then(|response| {
485+
if let MetadataResponse::Found(archive) = &*response {
486+
Some(archive.metadata.clone())
487+
} else {
488+
None
489+
}
490+
})
463491
};
464492

465493
(dist, hashes, metadata)
@@ -470,11 +498,12 @@ impl ResolutionGraph {
470498
/// lockfile.
471499
fn get_hashes(
472500
name: &PackageName,
501+
index: Option<&IndexUrl>,
473502
url: Option<&VerbatimParsedUrl>,
474503
version_id: &VersionId,
475504
version: &Version,
476505
preferences: &Preferences,
477-
index: &InMemoryIndex,
506+
in_memory: &InMemoryIndex,
478507
) -> Vec<HashDigest> {
479508
// 1. Look for hashes from the lockfile.
480509
if let Some(digests) = preferences.match_hashes(name, version) {
@@ -484,7 +513,7 @@ impl ResolutionGraph {
484513
}
485514

486515
// 2. Look for hashes for the distribution (i.e., the specific wheel or source distribution).
487-
if let Some(metadata_response) = index.distributions().get(version_id) {
516+
if let Some(metadata_response) = in_memory.distributions().get(version_id) {
488517
if let MetadataResponse::Found(ref archive) = *metadata_response {
489518
let mut digests = archive.hashes.clone();
490519
digests.sort_unstable();
@@ -496,7 +525,13 @@ impl ResolutionGraph {
496525

497526
// 3. Look for hashes from the registry, which are served at the package level.
498527
if url.is_none() {
499-
if let Some(versions_response) = index.packages().get(name) {
528+
let versions_response = if let Some(index) = index {
529+
in_memory.explicit().get(&(name.clone(), index.clone()))
530+
} else {
531+
in_memory.implicit().get(name)
532+
};
533+
534+
if let Some(versions_response) = versions_response {
500535
if let VersionsResponse::Found(ref version_maps) = *versions_response {
501536
if let Some(digests) = version_maps
502537
.iter()

crates/uv-resolver/src/resolver/batch_prefetch.rs

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ use rustc_hash::FxHashMap;
66
use tokio::sync::mpsc::Sender;
77
use tracing::{debug, trace};
88

9-
use distribution_types::{CompatibleDist, DistributionMetadata, IndexCapabilities};
9+
use distribution_types::{CompatibleDist, DistributionMetadata, IndexCapabilities, IndexUrl};
1010
use pep440_rs::Version;
1111

1212
use crate::candidate_selector::CandidateSelector;
@@ -47,11 +47,12 @@ impl BatchPrefetcher {
4747
pub(crate) fn prefetch_batches(
4848
&mut self,
4949
next: &PubGrubPackage,
50+
index: Option<&IndexUrl>,
5051
version: &Version,
5152
current_range: &Range<Version>,
5253
python_requirement: &PythonRequirement,
5354
request_sink: &Sender<Request>,
54-
index: &InMemoryIndex,
55+
in_memory: &InMemoryIndex,
5556
capabilities: &IndexCapabilities,
5657
selector: &CandidateSelector,
5758
markers: &ResolverMarkers,
@@ -73,10 +74,17 @@ impl BatchPrefetcher {
7374
let total_prefetch = min(num_tried, 50);
7475

7576
// This is immediate, we already fetched the version map.
76-
let versions_response = index
77-
.packages()
78-
.wait_blocking(name)
79-
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?;
77+
let versions_response = if let Some(index) = index {
78+
in_memory
79+
.explicit()
80+
.wait_blocking(&(name.clone(), index.clone()))
81+
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?
82+
} else {
83+
in_memory
84+
.implicit()
85+
.wait_blocking(name)
86+
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?
87+
};
8088

8189
let VersionsResponse::Found(ref version_map) = *versions_response else {
8290
return Ok(());
@@ -191,7 +199,7 @@ impl BatchPrefetcher {
191199
);
192200
prefetch_count += 1;
193201

194-
if index.distributions().register(candidate.version_id()) {
202+
if in_memory.distributions().register(candidate.version_id()) {
195203
let request = Request::from(dist);
196204
request_sink.blocking_send(request)?;
197205
}

crates/uv-resolver/src/resolver/fork_map.rs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,11 @@ impl<T> ForkMap<T> {
4444
!self.get(package_name, markers).is_empty()
4545
}
4646

47+
/// Returns `true` if the map contains any values for a package.
48+
pub(crate) fn contains_key(&self, package_name: &PackageName) -> bool {
49+
self.0.contains_key(package_name)
50+
}
51+
4752
/// Returns a list of values associated with a package that are compatible with the given fork.
4853
///
4954
/// Compatibility implies that the markers on the requirement that contained this value

crates/uv-resolver/src/resolver/index.rs

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
use std::hash::BuildHasherDefault;
22
use std::sync::Arc;
33

4-
use distribution_types::VersionId;
4+
use distribution_types::{IndexUrl, VersionId};
55
use once_map::OnceMap;
66
use rustc_hash::FxHasher;
77
use uv_normalize::PackageName;
@@ -16,7 +16,9 @@ pub struct InMemoryIndex(Arc<SharedInMemoryIndex>);
1616
struct SharedInMemoryIndex {
1717
/// A map from package name to the metadata for that package and the index where the metadata
1818
/// came from.
19-
packages: FxOnceMap<PackageName, Arc<VersionsResponse>>,
19+
implicit: FxOnceMap<PackageName, Arc<VersionsResponse>>,
20+
21+
explicit: FxOnceMap<(PackageName, IndexUrl), Arc<VersionsResponse>>,
2022

2123
/// A map from package ID to metadata for that distribution.
2224
distributions: FxOnceMap<VersionId, Arc<MetadataResponse>>,
@@ -26,8 +28,13 @@ pub(crate) type FxOnceMap<K, V> = OnceMap<K, V, BuildHasherDefault<FxHasher>>;
2628

2729
impl InMemoryIndex {
2830
/// Returns a reference to the package metadata map.
29-
pub fn packages(&self) -> &FxOnceMap<PackageName, Arc<VersionsResponse>> {
30-
&self.0.packages
31+
pub fn implicit(&self) -> &FxOnceMap<PackageName, Arc<VersionsResponse>> {
32+
&self.0.implicit
33+
}
34+
35+
/// Returns a reference to the package metadata map.
36+
pub fn explicit(&self) -> &FxOnceMap<(PackageName, IndexUrl), Arc<VersionsResponse>> {
37+
&self.0.explicit
3138
}
3239

3340
/// Returns a reference to the distribution metadata map.

0 commit comments

Comments
 (0)