Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow package lookups across multiple indexes via explicit opt-in #2815

Merged
merged 2 commits into from
Apr 3, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

6 changes: 6 additions & 0 deletions PIP_COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,12 @@ internal package, thus causing the malicious package to be installed instead of
package. See, for example, [the `torchtriton` attack](https://pytorch.org/blog/compromised-nightly-dependency/)
from December 2022.

As of v0.1.29, users can opt in to `pip`-style behavior for multiple indexes via the
`--index-strategy unsafe-any-match` command-line option, or the `UV_INDEX_STRATEGY` environment
variable. When enabled, uv will search for each package across all indexes, and consider all
available versions when resolving dependencies, prioritizing the `--extra-index-url` indexes over
the default index URL. (Versions that are duplicated _across_ indexes will be ignored.)

In the future, uv will support pinning packages to dedicated indexes (see: [#171](https://github.com/astral-sh/uv/issues/171)).
Additionally, [PEP 708](https://peps.python.org/pep-0708/) is a provisional standard that aims to
address the "dependency confusion" issue across package registries and installers.
Expand Down
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,9 @@ uv accepts the following command-line arguments as environment variables:
should be used with caution, as it can modify the system Python installation.
- `UV_NATIVE_TLS`: Equivalent to the `--native-tls` command-line argument. If set to `true`, uv
will use the system's trust store instead of the bundled `webpki-roots` crate.
- `UV_INDEX_STRATEGY`: Equivalent to the `--index-strategy` command-line argument. For example, if
set to `unsafe-any-match`, uv will consider versions of a given package available across all
index URLs, rather than limiting its search to the first index URL that contains the package.

In each case, the corresponding command-line argument takes precedence over an environment variable.

Expand Down
1 change: 1 addition & 0 deletions crates/uv-client/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ uv-auth = { workspace = true }
uv-cache = { workspace = true }
uv-fs = { workspace = true, features = ["tokio"] }
uv-normalize = { workspace = true }
uv-types = { workspace = true }
uv-version = { workspace = true }
uv-warnings = { workspace = true }
pypi-types = { workspace = true }
Expand Down
46 changes: 34 additions & 12 deletions crates/uv-client/src/registry_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ use pypi_types::{Metadata23, SimpleJson};
use uv_auth::KeyringProvider;
use uv_cache::{Cache, CacheBucket, WheelCache};
use uv_normalize::PackageName;
use uv_types::IndexStrategy;

use crate::base_client::{BaseClient, BaseClientBuilder};
use crate::cached_client::CacheControl;
Expand All @@ -35,6 +36,7 @@ use crate::{CachedClient, CachedClientError, Error, ErrorKind};
#[derive(Debug, Clone)]
pub struct RegistryClientBuilder<'a> {
index_urls: IndexUrls,
index_strategy: IndexStrategy,
keyring_provider: KeyringProvider,
native_tls: bool,
retries: u32,
Expand All @@ -49,6 +51,7 @@ impl RegistryClientBuilder<'_> {
pub fn new(cache: Cache) -> Self {
Self {
index_urls: IndexUrls::default(),
index_strategy: IndexStrategy::default(),
keyring_provider: KeyringProvider::default(),
native_tls: false,
cache,
Expand All @@ -68,6 +71,12 @@ impl<'a> RegistryClientBuilder<'a> {
self
}

#[must_use]
pub fn index_strategy(mut self, index_strategy: IndexStrategy) -> Self {
self.index_strategy = index_strategy;
self
}

#[must_use]
pub fn keyring_provider(mut self, keyring_provider: KeyringProvider) -> Self {
self.keyring_provider = keyring_provider;
Expand Down Expand Up @@ -147,6 +156,7 @@ impl<'a> RegistryClientBuilder<'a> {

RegistryClient {
index_urls: self.index_urls,
index_strategy: self.index_strategy,
cache: self.cache,
connectivity,
client,
Expand All @@ -160,6 +170,8 @@ impl<'a> RegistryClientBuilder<'a> {
pub struct RegistryClient {
/// The index URLs to use for fetching packages.
index_urls: IndexUrls,
/// The strategy to use when fetching across multiple indexes.
index_strategy: IndexStrategy,
/// The underlying HTTP client.
client: CachedClient,
/// Used for the remote wheel METADATA cache.
Expand Down Expand Up @@ -206,17 +218,23 @@ impl RegistryClient {
pub async fn simple(
&self,
package_name: &PackageName,
) -> Result<(IndexUrl, OwnedArchive<SimpleMetadata>), Error> {
) -> Result<Vec<(IndexUrl, OwnedArchive<SimpleMetadata>)>, Error> {
let mut it = self.index_urls.indexes().peekable();
if it.peek().is_none() {
return Err(ErrorKind::NoIndex(package_name.as_ref().to_string()).into());
}

let mut results = Vec::new();
for index in it {
let result = self.simple_single_index(package_name, index).await?;
match self.simple_single_index(package_name, index).await? {
Ok(metadata) => {
results.push((index.clone(), metadata));

return match result {
Ok(metadata) => Ok((index.clone(), metadata)),
// If we're only using the first match, we can stop here.
if self.index_strategy == IndexStrategy::FirstMatch {
break;
}
}
Err(CachedClientError::Client(err)) => match err.into_kind() {
ErrorKind::Offline(_) => continue,
ErrorKind::ReqwestError(err) => {
Expand All @@ -225,20 +243,24 @@ impl RegistryClient {
{
continue;
}
Err(ErrorKind::from(err).into())
return Err(ErrorKind::from(err).into());
}
other => Err(other.into()),
other => return Err(other.into()),
},
Err(CachedClientError::Callback(err)) => Err(err),
Err(CachedClientError::Callback(err)) => return Err(err),
};
}

match self.connectivity {
Connectivity::Online => {
Err(ErrorKind::PackageNotFound(package_name.to_string()).into())
}
Connectivity::Offline => Err(ErrorKind::Offline(package_name.to_string()).into()),
if results.is_empty() {
return match self.connectivity {
Connectivity::Online => {
Err(ErrorKind::PackageNotFound(package_name.to_string()).into())
}
Connectivity::Offline => Err(ErrorKind::Offline(package_name.to_string()).into()),
};
}

Ok(results)
}

async fn simple_single_index(
Expand Down
15 changes: 11 additions & 4 deletions crates/uv-dev/src/resolve_many.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,10 +47,17 @@ async fn find_latest_version(
client: &RegistryClient,
package_name: &PackageName,
) -> Option<Version> {
let (_, raw_simple_metadata) = client.simple(package_name).await.ok()?;
let simple_metadata = OwnedArchive::deserialize(&raw_simple_metadata);
let version = simple_metadata.into_iter().next()?.version;
Some(version)
client
.simple(package_name)
.await
.ok()
.into_iter()
.flatten()
.filter_map(|(_index, raw_simple_metadata)| {
let simple_metadata = OwnedArchive::deserialize(&raw_simple_metadata);
Some(simple_metadata.into_iter().next()?.version)
})
.max()
}

pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
Expand Down
55 changes: 32 additions & 23 deletions crates/uv-resolver/src/candidate_selector.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,7 +71,7 @@ impl CandidateSelector {
&'a self,
package_name: &'a PackageName,
range: &'a Range<Version>,
version_map: &'a VersionMap,
version_maps: &'a [VersionMap],
preferences: &'a Preferences,
installed_packages: &'a InstalledPackages,
exclusions: &'a Exclusions,
Expand Down Expand Up @@ -107,7 +107,10 @@ impl CandidateSelector {
}

// Check for a remote distribution that matches the preferred version
if let Some(file) = version_map.get(version) {
if let Some(file) = version_maps
.iter()
.find_map(|version_map| version_map.get(version))
{
return Some(Candidate::new(package_name, version, file));
}
}
Expand Down Expand Up @@ -163,33 +166,39 @@ impl CandidateSelector {
"selecting candidate for package {:?} with range {:?} with {} remote versions",
package_name,
range,
version_map.len()
version_maps.iter().map(VersionMap::len).sum::<usize>(),
);
match &self.resolution_strategy {
ResolutionStrategy::Highest => Self::select_candidate(
version_map.iter().rev(),
package_name,
range,
allow_prerelease,
),
ResolutionStrategy::Lowest => {
ResolutionStrategy::Highest => version_maps.iter().find_map(|version_map| {
Self::select_candidate(
version_map.iter().rev(),
package_name,
range,
allow_prerelease,
)
}),
ResolutionStrategy::Lowest => version_maps.iter().find_map(|version_map| {
Self::select_candidate(version_map.iter(), package_name, range, allow_prerelease)
}
}),
ResolutionStrategy::LowestDirect(direct_dependencies) => {
if direct_dependencies.contains(package_name) {
Self::select_candidate(
version_map.iter(),
package_name,
range,
allow_prerelease,
)
version_maps.iter().find_map(|version_map| {
Self::select_candidate(
version_map.iter(),
package_name,
range,
allow_prerelease,
)
})
} else {
Self::select_candidate(
version_map.iter().rev(),
package_name,
range,
allow_prerelease,
)
version_maps.iter().find_map(|version_map| {
Self::select_candidate(
version_map.iter().rev(),
package_name,
range,
allow_prerelease,
)
})
}
}
}
Expand Down
17 changes: 9 additions & 8 deletions crates/uv-resolver/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -209,14 +209,15 @@ impl NoSolutionError {
// we represent the state of the resolver at the time of failure.
if visited.contains(name) {
if let Some(response) = package_versions.get(name) {
if let VersionsResponse::Found(ref version_map) = *response {
available_versions.insert(
package.clone(),
version_map
.iter()
.map(|(version, _)| version.clone())
.collect(),
);
if let VersionsResponse::Found(ref version_maps) = *response {
for version_map in version_maps {
available_versions
.entry(package.clone())
.or_insert_with(BTreeSet::new)
.extend(
version_map.iter().map(|(version, _)| version.clone()),
);
}
}
}
}
Expand Down
28 changes: 16 additions & 12 deletions crates/uv-resolver/src/resolution.rs
Original file line number Diff line number Diff line change
Expand Up @@ -99,12 +99,14 @@ impl ResolutionGraph {
if let Some(hash) = preferences.match_hashes(package_name, version) {
hashes.insert(package_name.clone(), hash.to_vec());
} else if let Some(versions_response) = packages.get(package_name) {
if let VersionsResponse::Found(ref version_map) = *versions_response {
hashes.insert(package_name.clone(), {
let mut hash = version_map.hashes(version);
hash.sort_unstable();
hash
});
if let VersionsResponse::Found(ref version_maps) = *versions_response {
for version_map in version_maps {
if let Some(mut hash) = version_map.hashes(version) {
hash.sort_unstable();
hashes.insert(package_name.clone(), hash);
break;
}
}
}
}

Expand All @@ -127,12 +129,14 @@ impl ResolutionGraph {
if let Some(hash) = preferences.match_hashes(package_name, version) {
hashes.insert(package_name.clone(), hash.to_vec());
} else if let Some(versions_response) = packages.get(package_name) {
if let VersionsResponse::Found(ref version_map) = *versions_response {
hashes.insert(package_name.clone(), {
let mut hash = version_map.hashes(version);
hash.sort_unstable();
hash
});
if let VersionsResponse::Found(ref version_maps) = *versions_response {
for version_map in version_maps {
if let Some(mut hash) = version_map.hashes(version) {
hash.sort_unstable();
hashes.insert(package_name.clone(), hash);
break;
}
}
}
}

Expand Down
17 changes: 7 additions & 10 deletions crates/uv-resolver/src/resolver/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ use std::ops::Deref;
use std::sync::Arc;

use anyhow::Result;

use dashmap::{DashMap, DashSet};
use futures::{FutureExt, StreamExt};
use itertools::Itertools;
Expand Down Expand Up @@ -34,7 +33,6 @@ use uv_normalize::PackageName;
use uv_types::{BuildContext, Constraints, InstalledPackagesProvider, Overrides};

use crate::candidate_selector::{CandidateDist, CandidateSelector};

use crate::editables::Editables;
use crate::error::ResolveError;
use crate::manifest::Manifest;
Expand All @@ -54,7 +52,7 @@ pub use crate::resolver::provider::{
use crate::resolver::reporter::Facade;
pub use crate::resolver::reporter::{BuildId, Reporter};
use crate::yanks::AllowedYanks;
use crate::{DependencyMode, Exclusions, Options, VersionMap};
use crate::{DependencyMode, Exclusions, Options};

mod index;
mod locals;
Expand Down Expand Up @@ -632,23 +630,22 @@ impl<
.ok_or(ResolveError::Unregistered)?;
self.visited.insert(package_name.clone());

let empty_version_map = VersionMap::default();
let version_map = match *versions_response {
VersionsResponse::Found(ref version_map) => version_map,
let version_maps = match *versions_response {
VersionsResponse::Found(ref version_maps) => version_maps.as_slice(),
VersionsResponse::NoIndex => {
self.unavailable_packages
.insert(package_name.clone(), UnavailablePackage::NoIndex);
&empty_version_map
&[]
}
VersionsResponse::Offline => {
self.unavailable_packages
.insert(package_name.clone(), UnavailablePackage::Offline);
&empty_version_map
&[]
}
VersionsResponse::NotFound => {
self.unavailable_packages
.insert(package_name.clone(), UnavailablePackage::NotFound);
&empty_version_map
&[]
}
};

Expand All @@ -664,7 +661,7 @@ impl<
let Some(candidate) = self.selector.select(
package_name,
range,
version_map,
version_maps,
&self.preferences,
self.installed_packages,
&self.exclusions,
Expand Down
Loading
Loading