Skip to content

Commit a1ff0c1

Browse files
committed
Allow multiple indexes
1 parent 1925922 commit a1ff0c1

File tree

14 files changed

+695
-109
lines changed

14 files changed

+695
-109
lines changed

crates/uv-resolver/src/error.rs

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

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

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

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_pypi_types::VerbatimParsedUrl;
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
@@ -10,7 +10,7 @@ use rustc_hash::{FxBuildHasher, FxHashMap, FxHashSet};
1010
use uv_configuration::{Constraints, Overrides};
1111
use uv_distribution::Metadata;
1212
use uv_distribution_types::{
13-
Dist, DistributionMetadata, Name, ResolutionDiagnostic, ResolvedDist, VersionId,
13+
Dist, DistributionMetadata, IndexUrl, Name, ResolutionDiagnostic, ResolvedDist, VersionId,
1414
VersionOrUrlRef,
1515
};
1616
use uv_git::GitResolver;
@@ -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: 16 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,13 +6,12 @@ use rustc_hash::FxHashMap;
66
use tokio::sync::mpsc::Sender;
77
use tracing::{debug, trace};
88

9-
use uv_distribution_types::{CompatibleDist, DistributionMetadata, IndexCapabilities};
10-
use uv_pep440::Version;
11-
129
use crate::candidate_selector::CandidateSelector;
1310
use crate::pubgrub::{PubGrubPackage, PubGrubPackageInner};
1411
use crate::resolver::Request;
1512
use crate::{InMemoryIndex, PythonRequirement, ResolveError, ResolverMarkers, VersionsResponse};
13+
use uv_distribution_types::{CompatibleDist, DistributionMetadata, IndexCapabilities, IndexUrl};
14+
use uv_pep440::Version;
1615

1716
enum BatchPrefetchStrategy {
1817
/// Go through the next versions assuming the existing selection and its constraints
@@ -47,11 +46,12 @@ impl BatchPrefetcher {
4746
pub(crate) fn prefetch_batches(
4847
&mut self,
4948
next: &PubGrubPackage,
49+
index: Option<&IndexUrl>,
5050
version: &Version,
5151
current_range: &Range<Version>,
5252
python_requirement: &PythonRequirement,
5353
request_sink: &Sender<Request>,
54-
index: &InMemoryIndex,
54+
in_memory: &InMemoryIndex,
5555
capabilities: &IndexCapabilities,
5656
selector: &CandidateSelector,
5757
markers: &ResolverMarkers,
@@ -73,10 +73,17 @@ impl BatchPrefetcher {
7373
let total_prefetch = min(num_tried, 50);
7474

7575
// 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()))?;
76+
let versions_response = if let Some(index) = index {
77+
in_memory
78+
.explicit()
79+
.wait_blocking(&(name.clone(), index.clone()))
80+
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?
81+
} else {
82+
in_memory
83+
.implicit()
84+
.wait_blocking(name)
85+
.ok_or_else(|| ResolveError::UnregisteredTask(name.to_string()))?
86+
};
8087

8188
let VersionsResponse::Found(ref version_map) = *versions_response else {
8289
return Ok(());
@@ -191,7 +198,7 @@ impl BatchPrefetcher {
191198
);
192199
prefetch_count += 1;
193200

194-
if index.distributions().register(candidate.version_id()) {
201+
if in_memory.distributions().register(candidate.version_id()) {
195202
let request = Request::from(dist);
196203
request_sink.blocking_send(request)?;
197204
}

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
@@ -2,7 +2,7 @@ use std::hash::BuildHasherDefault;
22
use std::sync::Arc;
33

44
use rustc_hash::FxHasher;
5-
use uv_distribution_types::VersionId;
5+
use uv_distribution_types::{IndexUrl, VersionId};
66
use uv_normalize::PackageName;
77
use uv_once_map::OnceMap;
88

@@ -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)