mirror of
https://github.com/astral-sh/uv.git
synced 2025-09-26 20:19:08 +00:00
Allow package lookups across multiple indexes via explicit opt-in (#2815)
## Summary This partially revives https://github.com/astral-sh/uv/pull/2135 (with some modifications) to enable users to opt-in to looking for packages across multiple indexes. The behavior is such that, in version selection, we take _any_ compatible version from a "higher-priority" index over the compatible versions of a "lower-priority" index, even if that means we might accept an "older" version. Closes https://github.com/astral-sh/uv/issues/2775.
This commit is contained in:
parent
e0d55ef496
commit
34341bd6e9
23 changed files with 351 additions and 106 deletions
2
Cargo.lock
generated
2
Cargo.lock
generated
|
@ -4464,6 +4464,7 @@ dependencies = [
|
||||||
"uv-cache",
|
"uv-cache",
|
||||||
"uv-fs",
|
"uv-fs",
|
||||||
"uv-normalize",
|
"uv-normalize",
|
||||||
|
"uv-types",
|
||||||
"uv-version",
|
"uv-version",
|
||||||
"uv-warnings",
|
"uv-warnings",
|
||||||
"webpki-roots",
|
"webpki-roots",
|
||||||
|
@ -4785,6 +4786,7 @@ name = "uv-types"
|
||||||
version = "0.0.1"
|
version = "0.0.1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
|
"clap",
|
||||||
"distribution-types",
|
"distribution-types",
|
||||||
"itertools 0.12.1",
|
"itertools 0.12.1",
|
||||||
"once-map",
|
"once-map",
|
||||||
|
|
|
@ -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/)
|
package. See, for example, [the `torchtriton` attack](https://pytorch.org/blog/compromised-nightly-dependency/)
|
||||||
from December 2022.
|
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)).
|
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
|
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.
|
address the "dependency confusion" issue across package registries and installers.
|
||||||
|
|
|
@ -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.
|
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
|
- `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.
|
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.
|
In each case, the corresponding command-line argument takes precedence over an environment variable.
|
||||||
|
|
||||||
|
|
|
@ -15,6 +15,7 @@ uv-auth = { workspace = true }
|
||||||
uv-cache = { workspace = true }
|
uv-cache = { workspace = true }
|
||||||
uv-fs = { workspace = true, features = ["tokio"] }
|
uv-fs = { workspace = true, features = ["tokio"] }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
|
uv-types = { workspace = true }
|
||||||
uv-version = { workspace = true }
|
uv-version = { workspace = true }
|
||||||
uv-warnings = { workspace = true }
|
uv-warnings = { workspace = true }
|
||||||
pypi-types = { workspace = true }
|
pypi-types = { workspace = true }
|
||||||
|
|
|
@ -23,6 +23,7 @@ use pypi_types::{Metadata23, SimpleJson};
|
||||||
use uv_auth::KeyringProvider;
|
use uv_auth::KeyringProvider;
|
||||||
use uv_cache::{Cache, CacheBucket, WheelCache};
|
use uv_cache::{Cache, CacheBucket, WheelCache};
|
||||||
use uv_normalize::PackageName;
|
use uv_normalize::PackageName;
|
||||||
|
use uv_types::IndexStrategy;
|
||||||
|
|
||||||
use crate::base_client::{BaseClient, BaseClientBuilder};
|
use crate::base_client::{BaseClient, BaseClientBuilder};
|
||||||
use crate::cached_client::CacheControl;
|
use crate::cached_client::CacheControl;
|
||||||
|
@ -35,6 +36,7 @@ use crate::{CachedClient, CachedClientError, Error, ErrorKind};
|
||||||
#[derive(Debug, Clone)]
|
#[derive(Debug, Clone)]
|
||||||
pub struct RegistryClientBuilder<'a> {
|
pub struct RegistryClientBuilder<'a> {
|
||||||
index_urls: IndexUrls,
|
index_urls: IndexUrls,
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProvider,
|
||||||
native_tls: bool,
|
native_tls: bool,
|
||||||
retries: u32,
|
retries: u32,
|
||||||
|
@ -49,6 +51,7 @@ impl RegistryClientBuilder<'_> {
|
||||||
pub fn new(cache: Cache) -> Self {
|
pub fn new(cache: Cache) -> Self {
|
||||||
Self {
|
Self {
|
||||||
index_urls: IndexUrls::default(),
|
index_urls: IndexUrls::default(),
|
||||||
|
index_strategy: IndexStrategy::default(),
|
||||||
keyring_provider: KeyringProvider::default(),
|
keyring_provider: KeyringProvider::default(),
|
||||||
native_tls: false,
|
native_tls: false,
|
||||||
cache,
|
cache,
|
||||||
|
@ -68,6 +71,12 @@ impl<'a> RegistryClientBuilder<'a> {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[must_use]
|
||||||
|
pub fn index_strategy(mut self, index_strategy: IndexStrategy) -> Self {
|
||||||
|
self.index_strategy = index_strategy;
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn keyring_provider(mut self, keyring_provider: KeyringProvider) -> Self {
|
pub fn keyring_provider(mut self, keyring_provider: KeyringProvider) -> Self {
|
||||||
self.keyring_provider = keyring_provider;
|
self.keyring_provider = keyring_provider;
|
||||||
|
@ -147,6 +156,7 @@ impl<'a> RegistryClientBuilder<'a> {
|
||||||
|
|
||||||
RegistryClient {
|
RegistryClient {
|
||||||
index_urls: self.index_urls,
|
index_urls: self.index_urls,
|
||||||
|
index_strategy: self.index_strategy,
|
||||||
cache: self.cache,
|
cache: self.cache,
|
||||||
connectivity,
|
connectivity,
|
||||||
client,
|
client,
|
||||||
|
@ -160,6 +170,8 @@ impl<'a> RegistryClientBuilder<'a> {
|
||||||
pub struct RegistryClient {
|
pub struct RegistryClient {
|
||||||
/// The index URLs to use for fetching packages.
|
/// The index URLs to use for fetching packages.
|
||||||
index_urls: IndexUrls,
|
index_urls: IndexUrls,
|
||||||
|
/// The strategy to use when fetching across multiple indexes.
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
/// The underlying HTTP client.
|
/// The underlying HTTP client.
|
||||||
client: CachedClient,
|
client: CachedClient,
|
||||||
/// Used for the remote wheel METADATA cache.
|
/// Used for the remote wheel METADATA cache.
|
||||||
|
@ -206,17 +218,23 @@ impl RegistryClient {
|
||||||
pub async fn simple(
|
pub async fn simple(
|
||||||
&self,
|
&self,
|
||||||
package_name: &PackageName,
|
package_name: &PackageName,
|
||||||
) -> Result<(IndexUrl, OwnedArchive<SimpleMetadata>), Error> {
|
) -> Result<Vec<(IndexUrl, OwnedArchive<SimpleMetadata>)>, Error> {
|
||||||
let mut it = self.index_urls.indexes().peekable();
|
let mut it = self.index_urls.indexes().peekable();
|
||||||
if it.peek().is_none() {
|
if it.peek().is_none() {
|
||||||
return Err(ErrorKind::NoIndex(package_name.as_ref().to_string()).into());
|
return Err(ErrorKind::NoIndex(package_name.as_ref().to_string()).into());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let mut results = Vec::new();
|
||||||
for index in it {
|
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 {
|
// If we're only using the first match, we can stop here.
|
||||||
Ok(metadata) => Ok((index.clone(), metadata)),
|
if self.index_strategy == IndexStrategy::FirstMatch {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
Err(CachedClientError::Client(err)) => match err.into_kind() {
|
Err(CachedClientError::Client(err)) => match err.into_kind() {
|
||||||
ErrorKind::Offline(_) => continue,
|
ErrorKind::Offline(_) => continue,
|
||||||
ErrorKind::ReqwestError(err) => {
|
ErrorKind::ReqwestError(err) => {
|
||||||
|
@ -225,20 +243,24 @@ impl RegistryClient {
|
||||||
{
|
{
|
||||||
continue;
|
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 {
|
if results.is_empty() {
|
||||||
|
return match self.connectivity {
|
||||||
Connectivity::Online => {
|
Connectivity::Online => {
|
||||||
Err(ErrorKind::PackageNotFound(package_name.to_string()).into())
|
Err(ErrorKind::PackageNotFound(package_name.to_string()).into())
|
||||||
}
|
}
|
||||||
Connectivity::Offline => Err(ErrorKind::Offline(package_name.to_string()).into()),
|
Connectivity::Offline => Err(ErrorKind::Offline(package_name.to_string()).into()),
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn simple_single_index(
|
async fn simple_single_index(
|
||||||
|
|
|
@ -47,10 +47,17 @@ async fn find_latest_version(
|
||||||
client: &RegistryClient,
|
client: &RegistryClient,
|
||||||
package_name: &PackageName,
|
package_name: &PackageName,
|
||||||
) -> Option<Version> {
|
) -> Option<Version> {
|
||||||
let (_, raw_simple_metadata) = client.simple(package_name).await.ok()?;
|
client
|
||||||
|
.simple(package_name)
|
||||||
|
.await
|
||||||
|
.ok()
|
||||||
|
.into_iter()
|
||||||
|
.flatten()
|
||||||
|
.filter_map(|(_index, raw_simple_metadata)| {
|
||||||
let simple_metadata = OwnedArchive::deserialize(&raw_simple_metadata);
|
let simple_metadata = OwnedArchive::deserialize(&raw_simple_metadata);
|
||||||
let version = simple_metadata.into_iter().next()?.version;
|
Some(simple_metadata.into_iter().next()?.version)
|
||||||
Some(version)
|
})
|
||||||
|
.max()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
|
pub(crate) async fn resolve_many(args: ResolveManyArgs) -> Result<()> {
|
||||||
|
|
|
@ -71,7 +71,7 @@ impl CandidateSelector {
|
||||||
&'a self,
|
&'a self,
|
||||||
package_name: &'a PackageName,
|
package_name: &'a PackageName,
|
||||||
range: &'a Range<Version>,
|
range: &'a Range<Version>,
|
||||||
version_map: &'a VersionMap,
|
version_maps: &'a [VersionMap],
|
||||||
preferences: &'a Preferences,
|
preferences: &'a Preferences,
|
||||||
installed_packages: &'a InstalledPackages,
|
installed_packages: &'a InstalledPackages,
|
||||||
exclusions: &'a Exclusions,
|
exclusions: &'a Exclusions,
|
||||||
|
@ -107,7 +107,10 @@ impl CandidateSelector {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check for a remote distribution that matches the preferred version
|
// 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));
|
return Some(Candidate::new(package_name, version, file));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -163,33 +166,39 @@ impl CandidateSelector {
|
||||||
"selecting candidate for package {:?} with range {:?} with {} remote versions",
|
"selecting candidate for package {:?} with range {:?} with {} remote versions",
|
||||||
package_name,
|
package_name,
|
||||||
range,
|
range,
|
||||||
version_map.len()
|
version_maps.iter().map(VersionMap::len).sum::<usize>(),
|
||||||
);
|
);
|
||||||
match &self.resolution_strategy {
|
match &self.resolution_strategy {
|
||||||
ResolutionStrategy::Highest => Self::select_candidate(
|
ResolutionStrategy::Highest => version_maps.iter().find_map(|version_map| {
|
||||||
|
Self::select_candidate(
|
||||||
version_map.iter().rev(),
|
version_map.iter().rev(),
|
||||||
package_name,
|
package_name,
|
||||||
range,
|
range,
|
||||||
allow_prerelease,
|
allow_prerelease,
|
||||||
),
|
)
|
||||||
ResolutionStrategy::Lowest => {
|
}),
|
||||||
|
ResolutionStrategy::Lowest => version_maps.iter().find_map(|version_map| {
|
||||||
Self::select_candidate(version_map.iter(), package_name, range, allow_prerelease)
|
Self::select_candidate(version_map.iter(), package_name, range, allow_prerelease)
|
||||||
}
|
}),
|
||||||
ResolutionStrategy::LowestDirect(direct_dependencies) => {
|
ResolutionStrategy::LowestDirect(direct_dependencies) => {
|
||||||
if direct_dependencies.contains(package_name) {
|
if direct_dependencies.contains(package_name) {
|
||||||
|
version_maps.iter().find_map(|version_map| {
|
||||||
Self::select_candidate(
|
Self::select_candidate(
|
||||||
version_map.iter(),
|
version_map.iter(),
|
||||||
package_name,
|
package_name,
|
||||||
range,
|
range,
|
||||||
allow_prerelease,
|
allow_prerelease,
|
||||||
)
|
)
|
||||||
|
})
|
||||||
} else {
|
} else {
|
||||||
|
version_maps.iter().find_map(|version_map| {
|
||||||
Self::select_candidate(
|
Self::select_candidate(
|
||||||
version_map.iter().rev(),
|
version_map.iter().rev(),
|
||||||
package_name,
|
package_name,
|
||||||
range,
|
range,
|
||||||
allow_prerelease,
|
allow_prerelease,
|
||||||
)
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -209,13 +209,13 @@ impl NoSolutionError {
|
||||||
// we represent the state of the resolver at the time of failure.
|
// we represent the state of the resolver at the time of failure.
|
||||||
if visited.contains(name) {
|
if visited.contains(name) {
|
||||||
if let Some(response) = package_versions.get(name) {
|
if let Some(response) = package_versions.get(name) {
|
||||||
if let VersionsResponse::Found(ref version_map) = *response {
|
if let VersionsResponse::Found(ref version_maps) = *response {
|
||||||
available_versions.insert(
|
for version_map in version_maps {
|
||||||
package.clone(),
|
available_versions
|
||||||
version_map
|
.entry(package.clone())
|
||||||
.iter()
|
.or_insert_with(BTreeSet::new)
|
||||||
.map(|(version, _)| version.clone())
|
.extend(
|
||||||
.collect(),
|
version_map.iter().map(|(version, _)| version.clone()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -223,6 +223,7 @@ impl NoSolutionError {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
self.available_versions = available_versions;
|
self.available_versions = available_versions;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
|
@ -99,12 +99,14 @@ impl ResolutionGraph {
|
||||||
if let Some(hash) = preferences.match_hashes(package_name, version) {
|
if let Some(hash) = preferences.match_hashes(package_name, version) {
|
||||||
hashes.insert(package_name.clone(), hash.to_vec());
|
hashes.insert(package_name.clone(), hash.to_vec());
|
||||||
} else if let Some(versions_response) = packages.get(package_name) {
|
} else if let Some(versions_response) = packages.get(package_name) {
|
||||||
if let VersionsResponse::Found(ref version_map) = *versions_response {
|
if let VersionsResponse::Found(ref version_maps) = *versions_response {
|
||||||
hashes.insert(package_name.clone(), {
|
for version_map in version_maps {
|
||||||
let mut hash = version_map.hashes(version);
|
if let Some(mut hash) = version_map.hashes(version) {
|
||||||
hash.sort_unstable();
|
hash.sort_unstable();
|
||||||
hash
|
hashes.insert(package_name.clone(), hash);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -127,12 +129,14 @@ impl ResolutionGraph {
|
||||||
if let Some(hash) = preferences.match_hashes(package_name, version) {
|
if let Some(hash) = preferences.match_hashes(package_name, version) {
|
||||||
hashes.insert(package_name.clone(), hash.to_vec());
|
hashes.insert(package_name.clone(), hash.to_vec());
|
||||||
} else if let Some(versions_response) = packages.get(package_name) {
|
} else if let Some(versions_response) = packages.get(package_name) {
|
||||||
if let VersionsResponse::Found(ref version_map) = *versions_response {
|
if let VersionsResponse::Found(ref version_maps) = *versions_response {
|
||||||
hashes.insert(package_name.clone(), {
|
for version_map in version_maps {
|
||||||
let mut hash = version_map.hashes(version);
|
if let Some(mut hash) = version_map.hashes(version) {
|
||||||
hash.sort_unstable();
|
hash.sort_unstable();
|
||||||
hash
|
hashes.insert(package_name.clone(), hash);
|
||||||
});
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -6,7 +6,6 @@ use std::ops::Deref;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
|
|
||||||
use dashmap::{DashMap, DashSet};
|
use dashmap::{DashMap, DashSet};
|
||||||
use futures::{FutureExt, StreamExt};
|
use futures::{FutureExt, StreamExt};
|
||||||
use itertools::Itertools;
|
use itertools::Itertools;
|
||||||
|
@ -34,7 +33,6 @@ use uv_normalize::PackageName;
|
||||||
use uv_types::{BuildContext, Constraints, InstalledPackagesProvider, Overrides};
|
use uv_types::{BuildContext, Constraints, InstalledPackagesProvider, Overrides};
|
||||||
|
|
||||||
use crate::candidate_selector::{CandidateDist, CandidateSelector};
|
use crate::candidate_selector::{CandidateDist, CandidateSelector};
|
||||||
|
|
||||||
use crate::editables::Editables;
|
use crate::editables::Editables;
|
||||||
use crate::error::ResolveError;
|
use crate::error::ResolveError;
|
||||||
use crate::manifest::Manifest;
|
use crate::manifest::Manifest;
|
||||||
|
@ -54,7 +52,7 @@ pub use crate::resolver::provider::{
|
||||||
use crate::resolver::reporter::Facade;
|
use crate::resolver::reporter::Facade;
|
||||||
pub use crate::resolver::reporter::{BuildId, Reporter};
|
pub use crate::resolver::reporter::{BuildId, Reporter};
|
||||||
use crate::yanks::AllowedYanks;
|
use crate::yanks::AllowedYanks;
|
||||||
use crate::{DependencyMode, Exclusions, Options, VersionMap};
|
use crate::{DependencyMode, Exclusions, Options};
|
||||||
|
|
||||||
mod index;
|
mod index;
|
||||||
mod locals;
|
mod locals;
|
||||||
|
@ -632,23 +630,22 @@ impl<
|
||||||
.ok_or(ResolveError::Unregistered)?;
|
.ok_or(ResolveError::Unregistered)?;
|
||||||
self.visited.insert(package_name.clone());
|
self.visited.insert(package_name.clone());
|
||||||
|
|
||||||
let empty_version_map = VersionMap::default();
|
let version_maps = match *versions_response {
|
||||||
let version_map = match *versions_response {
|
VersionsResponse::Found(ref version_maps) => version_maps.as_slice(),
|
||||||
VersionsResponse::Found(ref version_map) => version_map,
|
|
||||||
VersionsResponse::NoIndex => {
|
VersionsResponse::NoIndex => {
|
||||||
self.unavailable_packages
|
self.unavailable_packages
|
||||||
.insert(package_name.clone(), UnavailablePackage::NoIndex);
|
.insert(package_name.clone(), UnavailablePackage::NoIndex);
|
||||||
&empty_version_map
|
&[]
|
||||||
}
|
}
|
||||||
VersionsResponse::Offline => {
|
VersionsResponse::Offline => {
|
||||||
self.unavailable_packages
|
self.unavailable_packages
|
||||||
.insert(package_name.clone(), UnavailablePackage::Offline);
|
.insert(package_name.clone(), UnavailablePackage::Offline);
|
||||||
&empty_version_map
|
&[]
|
||||||
}
|
}
|
||||||
VersionsResponse::NotFound => {
|
VersionsResponse::NotFound => {
|
||||||
self.unavailable_packages
|
self.unavailable_packages
|
||||||
.insert(package_name.clone(), UnavailablePackage::NotFound);
|
.insert(package_name.clone(), UnavailablePackage::NotFound);
|
||||||
&empty_version_map
|
&[]
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -664,7 +661,7 @@ impl<
|
||||||
let Some(candidate) = self.selector.select(
|
let Some(candidate) = self.selector.select(
|
||||||
package_name,
|
package_name,
|
||||||
range,
|
range,
|
||||||
version_map,
|
version_maps,
|
||||||
&self.preferences,
|
&self.preferences,
|
||||||
self.installed_packages,
|
self.installed_packages,
|
||||||
&self.exclusions,
|
&self.exclusions,
|
||||||
|
|
|
@ -22,7 +22,7 @@ pub type WheelMetadataResult = Result<Metadata23, uv_distribution::Error>;
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum VersionsResponse {
|
pub enum VersionsResponse {
|
||||||
/// The package was found in the registry with the included versions
|
/// The package was found in the registry with the included versions
|
||||||
Found(VersionMap),
|
Found(Vec<VersionMap>),
|
||||||
/// The package was not found in the registry
|
/// The package was not found in the registry
|
||||||
NotFound,
|
NotFound,
|
||||||
/// The package was not found in the local registry
|
/// The package was not found in the local registry
|
||||||
|
@ -113,7 +113,11 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider
|
||||||
// If the "Simple API" request was successful, convert to `VersionMap` on the Tokio
|
// If the "Simple API" request was successful, convert to `VersionMap` on the Tokio
|
||||||
// threadpool, since it can be slow.
|
// threadpool, since it can be slow.
|
||||||
match result {
|
match result {
|
||||||
Ok((index, metadata)) => Ok(VersionsResponse::Found(VersionMap::from_metadata(
|
Ok(results) => Ok(VersionsResponse::Found(
|
||||||
|
results
|
||||||
|
.into_iter()
|
||||||
|
.map(|(index, metadata)| {
|
||||||
|
VersionMap::from_metadata(
|
||||||
metadata,
|
metadata,
|
||||||
package_name,
|
package_name,
|
||||||
&index,
|
&index,
|
||||||
|
@ -124,18 +128,21 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider
|
||||||
self.flat_index.get(package_name).cloned(),
|
self.flat_index.get(package_name).cloned(),
|
||||||
&self.no_binary,
|
&self.no_binary,
|
||||||
&self.no_build,
|
&self.no_build,
|
||||||
))),
|
)
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
)),
|
||||||
Err(err) => match err.into_kind() {
|
Err(err) => match err.into_kind() {
|
||||||
uv_client::ErrorKind::PackageNotFound(_) => {
|
uv_client::ErrorKind::PackageNotFound(_) => {
|
||||||
if let Some(flat_index) = self.flat_index.get(package_name).cloned() {
|
if let Some(flat_index) = self.flat_index.get(package_name).cloned() {
|
||||||
Ok(VersionsResponse::Found(VersionMap::from(flat_index)))
|
Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
|
||||||
} else {
|
} else {
|
||||||
Ok(VersionsResponse::NotFound)
|
Ok(VersionsResponse::NotFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
uv_client::ErrorKind::NoIndex(_) => {
|
uv_client::ErrorKind::NoIndex(_) => {
|
||||||
if let Some(flat_index) = self.flat_index.get(package_name).cloned() {
|
if let Some(flat_index) = self.flat_index.get(package_name).cloned() {
|
||||||
Ok(VersionsResponse::Found(VersionMap::from(flat_index)))
|
Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
|
||||||
} else if self.flat_index.offline() {
|
} else if self.flat_index.offline() {
|
||||||
Ok(VersionsResponse::Offline)
|
Ok(VersionsResponse::Offline)
|
||||||
} else {
|
} else {
|
||||||
|
@ -144,7 +151,7 @@ impl<'a, Context: BuildContext + Send + Sync> ResolverProvider
|
||||||
}
|
}
|
||||||
uv_client::ErrorKind::Offline(_) => {
|
uv_client::ErrorKind::Offline(_) => {
|
||||||
if let Some(flat_index) = self.flat_index.get(package_name).cloned() {
|
if let Some(flat_index) = self.flat_index.get(package_name).cloned() {
|
||||||
Ok(VersionsResponse::Found(VersionMap::from(flat_index)))
|
Ok(VersionsResponse::Found(vec![VersionMap::from(flat_index)]))
|
||||||
} else {
|
} else {
|
||||||
Ok(VersionsResponse::Offline)
|
Ok(VersionsResponse::Offline)
|
||||||
}
|
}
|
||||||
|
|
|
@ -174,16 +174,10 @@ impl VersionMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Return the [`Hashes`] for the given version, if any.
|
/// Return the [`Hashes`] for the given version, if any.
|
||||||
pub(crate) fn hashes(&self, version: &Version) -> Vec<Hashes> {
|
pub(crate) fn hashes(&self, version: &Version) -> Option<Vec<Hashes>> {
|
||||||
match self.inner {
|
match self.inner {
|
||||||
VersionMapInner::Eager(ref map) => map
|
VersionMapInner::Eager(ref map) => map.get(version).map(|file| file.hashes().to_vec()),
|
||||||
.get(version)
|
VersionMapInner::Lazy(ref lazy) => lazy.get(version).map(|file| file.hashes().to_vec()),
|
||||||
.map(|file| file.hashes().to_vec())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
VersionMapInner::Lazy(ref lazy) => lazy
|
|
||||||
.get(version)
|
|
||||||
.map(|file| file.hashes().to_vec())
|
|
||||||
.unwrap_or_default(),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -21,6 +21,7 @@ uv-interpreter = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
|
|
||||||
anyhow = { workspace = true }
|
anyhow = { workspace = true }
|
||||||
|
clap = { workspace = true, features = ["derive"], optional = true }
|
||||||
itertools = { workspace = true }
|
itertools = { workspace = true }
|
||||||
rustc-hash = { workspace = true }
|
rustc-hash = { workspace = true }
|
||||||
serde = { workspace = true, optional = true }
|
serde = { workspace = true, optional = true }
|
||||||
|
|
|
@ -211,6 +211,29 @@ impl NoBuild {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default, Clone, Hash, Eq, PartialEq)]
|
||||||
|
#[cfg_attr(feature = "clap", derive(clap::ValueEnum))]
|
||||||
|
pub enum IndexStrategy {
|
||||||
|
/// Only use results from the first index that returns a match for a given package name.
|
||||||
|
///
|
||||||
|
/// While this differs from pip's behavior, it's the default index strategy as it's the most
|
||||||
|
/// secure.
|
||||||
|
#[default]
|
||||||
|
FirstMatch,
|
||||||
|
/// Search for every package name across all indexes, exhausting the versions from the first
|
||||||
|
/// index before moving on to the next.
|
||||||
|
///
|
||||||
|
/// In this strategy, we look for every package across all indexes. When resolving, we attempt
|
||||||
|
/// to use versions from the indexes in order, such that we exhaust all available versions from
|
||||||
|
/// the first index before moving on to the next. Further, if a version is found to be
|
||||||
|
/// incompatible in the first index, we do not reconsider that version in subsequent indexes,
|
||||||
|
/// even if the secondary index might contain compatible versions (e.g., variants of the same
|
||||||
|
/// versions with different ABI tags or Python version constraints).
|
||||||
|
///
|
||||||
|
/// See: https://peps.python.org/pep-0708/
|
||||||
|
UnsafeAnyMatch,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
|
@ -31,7 +31,7 @@ uv-interpreter = { workspace = true }
|
||||||
uv-normalize = { workspace = true }
|
uv-normalize = { workspace = true }
|
||||||
uv-requirements = { workspace = true }
|
uv-requirements = { workspace = true }
|
||||||
uv-resolver = { workspace = true, features = ["clap"] }
|
uv-resolver = { workspace = true, features = ["clap"] }
|
||||||
uv-types = { workspace = true }
|
uv-types = { workspace = true, features = ["clap"] }
|
||||||
uv-virtualenv = { workspace = true }
|
uv-virtualenv = { workspace = true }
|
||||||
uv-warnings = { workspace = true }
|
uv-warnings = { workspace = true }
|
||||||
|
|
||||||
|
|
|
@ -36,8 +36,8 @@ use uv_resolver::{
|
||||||
OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver,
|
OptionsBuilder, PreReleaseMode, PythonRequirement, ResolutionMode, Resolver,
|
||||||
};
|
};
|
||||||
use uv_types::{
|
use uv_types::{
|
||||||
BuildIsolation, ConfigSettings, Constraints, EmptyInstalledPackages, InFlight, NoBinary,
|
BuildIsolation, ConfigSettings, Constraints, EmptyInstalledPackages, InFlight, IndexStrategy,
|
||||||
NoBuild, Overrides, SetupPyStrategy, Upgrade,
|
NoBinary, NoBuild, Overrides, SetupPyStrategy, Upgrade,
|
||||||
};
|
};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
@ -67,6 +67,7 @@ pub(crate) async fn pip_compile(
|
||||||
include_find_links: bool,
|
include_find_links: bool,
|
||||||
include_marker_expression: bool,
|
include_marker_expression: bool,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProvider,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
config_settings: ConfigSettings,
|
config_settings: ConfigSettings,
|
||||||
|
@ -210,6 +211,7 @@ pub(crate) async fn pip_compile(
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
|
.index_strategy(index_strategy)
|
||||||
.keyring_provider(keyring_provider)
|
.keyring_provider(keyring_provider)
|
||||||
.markers(&markers)
|
.markers(&markers)
|
||||||
.platform(interpreter.platform())
|
.platform(interpreter.platform())
|
||||||
|
|
|
@ -38,8 +38,8 @@ use uv_resolver::{
|
||||||
Preference, ResolutionGraph, ResolutionMode, Resolver,
|
Preference, ResolutionGraph, ResolutionMode, Resolver,
|
||||||
};
|
};
|
||||||
use uv_types::{
|
use uv_types::{
|
||||||
BuildIsolation, ConfigSettings, Constraints, InFlight, NoBinary, NoBuild, Overrides, Reinstall,
|
BuildIsolation, ConfigSettings, Constraints, InFlight, IndexStrategy, NoBinary, NoBuild,
|
||||||
SetupPyStrategy, Upgrade,
|
Overrides, Reinstall, SetupPyStrategy, Upgrade,
|
||||||
};
|
};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
@ -61,6 +61,7 @@ pub(crate) async fn pip_install(
|
||||||
dependency_mode: DependencyMode,
|
dependency_mode: DependencyMode,
|
||||||
upgrade: Upgrade,
|
upgrade: Upgrade,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProvider,
|
||||||
reinstall: Reinstall,
|
reinstall: Reinstall,
|
||||||
link_mode: LinkMode,
|
link_mode: LinkMode,
|
||||||
|
@ -195,6 +196,7 @@ pub(crate) async fn pip_install(
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
|
.index_strategy(index_strategy)
|
||||||
.keyring_provider(keyring_provider)
|
.keyring_provider(keyring_provider)
|
||||||
.markers(markers)
|
.markers(markers)
|
||||||
.platform(interpreter.platform())
|
.platform(interpreter.platform())
|
||||||
|
|
|
@ -28,8 +28,8 @@ use uv_requirements::{
|
||||||
};
|
};
|
||||||
use uv_resolver::{DependencyMode, InMemoryIndex, Manifest, OptionsBuilder, Resolver};
|
use uv_resolver::{DependencyMode, InMemoryIndex, Manifest, OptionsBuilder, Resolver};
|
||||||
use uv_types::{
|
use uv_types::{
|
||||||
BuildIsolation, ConfigSettings, EmptyInstalledPackages, InFlight, NoBinary, NoBuild, Reinstall,
|
BuildIsolation, ConfigSettings, EmptyInstalledPackages, InFlight, IndexStrategy, NoBinary,
|
||||||
SetupPyStrategy,
|
NoBuild, Reinstall, SetupPyStrategy,
|
||||||
};
|
};
|
||||||
use uv_warnings::warn_user;
|
use uv_warnings::warn_user;
|
||||||
|
|
||||||
|
@ -45,6 +45,7 @@ pub(crate) async fn pip_sync(
|
||||||
link_mode: LinkMode,
|
link_mode: LinkMode,
|
||||||
compile: bool,
|
compile: bool,
|
||||||
index_locations: IndexLocations,
|
index_locations: IndexLocations,
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProvider,
|
||||||
setup_py: SetupPyStrategy,
|
setup_py: SetupPyStrategy,
|
||||||
connectivity: Connectivity,
|
connectivity: Connectivity,
|
||||||
|
@ -144,6 +145,7 @@ pub(crate) async fn pip_sync(
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
|
.index_strategy(index_strategy)
|
||||||
.keyring_provider(keyring_provider)
|
.keyring_provider(keyring_provider)
|
||||||
.markers(venv.interpreter().markers())
|
.markers(venv.interpreter().markers())
|
||||||
.platform(venv.interpreter().platform())
|
.platform(venv.interpreter().platform())
|
||||||
|
|
|
@ -21,7 +21,8 @@ use uv_fs::Simplified;
|
||||||
use uv_interpreter::{find_default_python, find_requested_python, Error};
|
use uv_interpreter::{find_default_python, find_requested_python, Error};
|
||||||
use uv_resolver::{InMemoryIndex, OptionsBuilder};
|
use uv_resolver::{InMemoryIndex, OptionsBuilder};
|
||||||
use uv_types::{
|
use uv_types::{
|
||||||
BuildContext, BuildIsolation, ConfigSettings, InFlight, NoBinary, NoBuild, SetupPyStrategy,
|
BuildContext, BuildIsolation, ConfigSettings, InFlight, IndexStrategy, NoBinary, NoBuild,
|
||||||
|
SetupPyStrategy,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::commands::ExitStatus;
|
use crate::commands::ExitStatus;
|
||||||
|
@ -34,6 +35,7 @@ pub(crate) async fn venv(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
python_request: Option<&str>,
|
python_request: Option<&str>,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProvider,
|
||||||
prompt: uv_virtualenv::Prompt,
|
prompt: uv_virtualenv::Prompt,
|
||||||
system_site_packages: bool,
|
system_site_packages: bool,
|
||||||
|
@ -48,6 +50,7 @@ pub(crate) async fn venv(
|
||||||
path,
|
path,
|
||||||
python_request,
|
python_request,
|
||||||
index_locations,
|
index_locations,
|
||||||
|
index_strategy,
|
||||||
keyring_provider,
|
keyring_provider,
|
||||||
prompt,
|
prompt,
|
||||||
system_site_packages,
|
system_site_packages,
|
||||||
|
@ -93,6 +96,7 @@ async fn venv_impl(
|
||||||
path: &Path,
|
path: &Path,
|
||||||
python_request: Option<&str>,
|
python_request: Option<&str>,
|
||||||
index_locations: &IndexLocations,
|
index_locations: &IndexLocations,
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
keyring_provider: KeyringProvider,
|
keyring_provider: KeyringProvider,
|
||||||
prompt: uv_virtualenv::Prompt,
|
prompt: uv_virtualenv::Prompt,
|
||||||
system_site_packages: bool,
|
system_site_packages: bool,
|
||||||
|
@ -150,6 +154,7 @@ async fn venv_impl(
|
||||||
let client = RegistryClientBuilder::new(cache.clone())
|
let client = RegistryClientBuilder::new(cache.clone())
|
||||||
.native_tls(native_tls)
|
.native_tls(native_tls)
|
||||||
.index_urls(index_locations.index_urls())
|
.index_urls(index_locations.index_urls())
|
||||||
|
.index_strategy(index_strategy)
|
||||||
.keyring_provider(keyring_provider)
|
.keyring_provider(keyring_provider)
|
||||||
.connectivity(connectivity)
|
.connectivity(connectivity)
|
||||||
.markers(interpreter.markers())
|
.markers(interpreter.markers())
|
||||||
|
|
|
@ -124,7 +124,7 @@ pub(crate) fn setup_logging(
|
||||||
}
|
}
|
||||||
Level::Verbose | Level::ExtraVerbose => {
|
Level::Verbose | Level::ExtraVerbose => {
|
||||||
// Show `DEBUG` messages from the CLI crate, but allow `RUST_LOG` to override.
|
// Show `DEBUG` messages from the CLI crate, but allow `RUST_LOG` to override.
|
||||||
Directive::from_str("uv=debug").unwrap()
|
Directive::from_str("uv=trace").unwrap()
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
|
@ -20,11 +20,11 @@ use uv_interpreter::PythonVersion;
|
||||||
use uv_normalize::{ExtraName, PackageName};
|
use uv_normalize::{ExtraName, PackageName};
|
||||||
use uv_requirements::{ExtrasSpecification, RequirementsSource};
|
use uv_requirements::{ExtrasSpecification, RequirementsSource};
|
||||||
use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode};
|
use uv_resolver::{AnnotationStyle, DependencyMode, PreReleaseMode, ResolutionMode};
|
||||||
use uv_types::NoBinary;
|
|
||||||
use uv_types::{
|
use uv_types::{
|
||||||
ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, Reinstall, SetupPyStrategy,
|
ConfigSettingEntry, ConfigSettings, NoBuild, PackageNameSpecifier, Reinstall, SetupPyStrategy,
|
||||||
Upgrade,
|
Upgrade,
|
||||||
};
|
};
|
||||||
|
use uv_types::{IndexStrategy, NoBinary};
|
||||||
|
|
||||||
use crate::commands::{extra_name_with_clap_error, ExitStatus, ListFormat, VersionFormat};
|
use crate::commands::{extra_name_with_clap_error, ExitStatus, ListFormat, VersionFormat};
|
||||||
use crate::compat::CompatArgs;
|
use crate::compat::CompatArgs;
|
||||||
|
@ -384,6 +384,15 @@ struct PipCompileArgs {
|
||||||
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
||||||
no_index: bool,
|
no_index: bool,
|
||||||
|
|
||||||
|
/// The strategy to use when resolving against multiple index URLs.
|
||||||
|
///
|
||||||
|
/// By default, `uv` will stop at the first index on which a given package is available, and
|
||||||
|
/// limit resolutions to those present on that first index. This prevents "dependency confusion"
|
||||||
|
/// attacks, whereby an attack can upload a malicious package under the same name to a secondary
|
||||||
|
/// index.
|
||||||
|
#[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")]
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
|
|
||||||
/// Attempt to use `keyring` for authentication for index urls
|
/// Attempt to use `keyring` for authentication for index urls
|
||||||
///
|
///
|
||||||
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
||||||
|
@ -570,6 +579,15 @@ struct PipSyncArgs {
|
||||||
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
||||||
no_index: bool,
|
no_index: bool,
|
||||||
|
|
||||||
|
/// The strategy to use when resolving against multiple index URLs.
|
||||||
|
///
|
||||||
|
/// By default, `uv` will stop at the first index on which a given package is available, and
|
||||||
|
/// limit resolutions to those present on that first index. This prevents "dependency confusion"
|
||||||
|
/// attacks, whereby an attack can upload a malicious package under the same name to a secondary
|
||||||
|
/// index.
|
||||||
|
#[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")]
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
|
|
||||||
/// Attempt to use `keyring` for authentication for index urls
|
/// Attempt to use `keyring` for authentication for index urls
|
||||||
///
|
///
|
||||||
/// Function's similar to `pip`'s `--keyring-provider subprocess` argument,
|
/// Function's similar to `pip`'s `--keyring-provider subprocess` argument,
|
||||||
|
@ -835,6 +853,15 @@ struct PipInstallArgs {
|
||||||
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
||||||
no_index: bool,
|
no_index: bool,
|
||||||
|
|
||||||
|
/// The strategy to use when resolving against multiple index URLs.
|
||||||
|
///
|
||||||
|
/// By default, `uv` will stop at the first index on which a given package is available, and
|
||||||
|
/// limit resolutions to those present on that first index. This prevents "dependency confusion"
|
||||||
|
/// attacks, whereby an attack can upload a malicious package under the same name to a secondary
|
||||||
|
/// index.
|
||||||
|
#[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")]
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
|
|
||||||
/// Attempt to use `keyring` for authentication for index urls
|
/// Attempt to use `keyring` for authentication for index urls
|
||||||
///
|
///
|
||||||
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
||||||
|
@ -1336,6 +1363,15 @@ struct VenvArgs {
|
||||||
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
#[clap(long, conflicts_with = "index_url", conflicts_with = "extra_index_url")]
|
||||||
no_index: bool,
|
no_index: bool,
|
||||||
|
|
||||||
|
/// The strategy to use when resolving against multiple index URLs.
|
||||||
|
///
|
||||||
|
/// By default, `uv` will stop at the first index on which a given package is available, and
|
||||||
|
/// limit resolutions to those present on that first index. This prevents "dependency confusion"
|
||||||
|
/// attacks, whereby an attack can upload a malicious package under the same name to a secondary
|
||||||
|
/// index.
|
||||||
|
#[clap(long, default_value_t, value_enum, env = "UV_INDEX_STRATEGY")]
|
||||||
|
index_strategy: IndexStrategy,
|
||||||
|
|
||||||
/// Attempt to use `keyring` for authentication for index urls
|
/// Attempt to use `keyring` for authentication for index urls
|
||||||
///
|
///
|
||||||
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
/// Due to not having Python imports, only `--keyring-provider subprocess` argument is currently
|
||||||
|
@ -1552,6 +1588,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.emit_find_links,
|
args.emit_find_links,
|
||||||
args.emit_marker_expression,
|
args.emit_marker_expression,
|
||||||
index_urls,
|
index_urls,
|
||||||
|
args.index_strategy,
|
||||||
args.keyring_provider,
|
args.keyring_provider,
|
||||||
setup_py,
|
setup_py,
|
||||||
config_settings,
|
config_settings,
|
||||||
|
@ -1608,6 +1645,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
args.link_mode,
|
args.link_mode,
|
||||||
args.compile,
|
args.compile,
|
||||||
index_urls,
|
index_urls,
|
||||||
|
args.index_strategy,
|
||||||
args.keyring_provider,
|
args.keyring_provider,
|
||||||
setup_py,
|
setup_py,
|
||||||
if args.offline {
|
if args.offline {
|
||||||
|
@ -1701,6 +1739,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
dependency_mode,
|
dependency_mode,
|
||||||
upgrade,
|
upgrade,
|
||||||
index_urls,
|
index_urls,
|
||||||
|
args.index_strategy,
|
||||||
args.keyring_provider,
|
args.keyring_provider,
|
||||||
reinstall,
|
reinstall,
|
||||||
args.link_mode,
|
args.link_mode,
|
||||||
|
@ -1833,6 +1872,7 @@ async fn run() -> Result<ExitStatus> {
|
||||||
&args.name,
|
&args.name,
|
||||||
args.python.as_deref(),
|
args.python.as_deref(),
|
||||||
&index_locations,
|
&index_locations,
|
||||||
|
args.index_strategy,
|
||||||
args.keyring_provider,
|
args.keyring_provider,
|
||||||
uv_virtualenv::Prompt::from_args(prompt),
|
uv_virtualenv::Prompt::from_args(prompt),
|
||||||
args.system_site_packages,
|
args.system_site_packages,
|
||||||
|
|
|
@ -139,13 +139,23 @@ impl TestContext {
|
||||||
/// * Set a cutoff for versions used in the resolution so the snapshots don't change after a new release.
|
/// * Set a cutoff for versions used in the resolution so the snapshots don't change after a new release.
|
||||||
/// * Set the venv to a fresh `.venv` in `temp_dir`.
|
/// * Set the venv to a fresh `.venv` in `temp_dir`.
|
||||||
pub fn compile(&self) -> std::process::Command {
|
pub fn compile(&self) -> std::process::Command {
|
||||||
|
let mut command = self.compile_without_exclude_newer();
|
||||||
|
command.arg("--exclude-newer").arg(EXCLUDE_NEWER);
|
||||||
|
command
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a `pip compile` command with no `--exclude-newer` option.
|
||||||
|
///
|
||||||
|
/// One should avoid using this in tests to the extent possible because
|
||||||
|
/// it can result in tests failing when the index state changes. Therefore,
|
||||||
|
/// if you use this, there should be some other kind of mitigation in place.
|
||||||
|
/// For example, pinning package versions.
|
||||||
|
pub fn compile_without_exclude_newer(&self) -> std::process::Command {
|
||||||
let mut cmd = std::process::Command::new(get_bin());
|
let mut cmd = std::process::Command::new(get_bin());
|
||||||
cmd.arg("pip")
|
cmd.arg("pip")
|
||||||
.arg("compile")
|
.arg("compile")
|
||||||
.arg("--cache-dir")
|
.arg("--cache-dir")
|
||||||
.arg(self.cache_dir.path())
|
.arg(self.cache_dir.path())
|
||||||
.arg("--exclude-newer")
|
|
||||||
.arg(EXCLUDE_NEWER)
|
|
||||||
.env("VIRTUAL_ENV", self.venv.as_os_str())
|
.env("VIRTUAL_ENV", self.venv.as_os_str())
|
||||||
.current_dir(self.temp_dir.path());
|
.current_dir(self.temp_dir.path());
|
||||||
|
|
||||||
|
|
|
@ -7040,3 +7040,110 @@ requires-python = ">3.8"
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Install a package via `--extra-index-url`.
|
||||||
|
///
|
||||||
|
/// If the package exists exist on the "extra" index, but at an incompatible version, the
|
||||||
|
/// resolution should fail by default (even though a compatible version exists on the "primary"
|
||||||
|
/// index).
|
||||||
|
#[test]
|
||||||
|
fn compile_index_url_first_match() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("jinja2==3.1.0")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile()
|
||||||
|
.arg("--index-url")
|
||||||
|
.arg("https://pypi.org/simple")
|
||||||
|
.arg("--extra-index-url")
|
||||||
|
.arg("https://download.pytorch.org/whl/cpu")
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--no-deps"), @r###"
|
||||||
|
success: false
|
||||||
|
exit_code: 1
|
||||||
|
----- stdout -----
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
× No solution found when resolving dependencies:
|
||||||
|
╰─▶ Because there is no version of jinja2==3.1.0 and you require
|
||||||
|
jinja2==3.1.0, we can conclude that the requirements are unsatisfiable.
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install a package via `--extra-index-url`.
|
||||||
|
///
|
||||||
|
/// If the package exists exist on the "extra" index, but at an incompatible version, the
|
||||||
|
/// resolution should fallback to the "primary" index when `--index-strategy unsafe-any-match`
|
||||||
|
/// is provided.
|
||||||
|
#[test]
|
||||||
|
fn compile_index_url_fallback() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("jinja2==3.1.0")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile()
|
||||||
|
.arg("--index-strategy")
|
||||||
|
.arg("unsafe-any-match")
|
||||||
|
.arg("--index-url")
|
||||||
|
.arg("https://pypi.org/simple")
|
||||||
|
.arg("--extra-index-url")
|
||||||
|
.arg("https://download.pytorch.org/whl/cpu")
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--no-deps"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --cache-dir [CACHE_DIR] --exclude-newer 2024-03-25T00:00:00Z --index-strategy unsafe-any-match requirements.in --no-deps
|
||||||
|
jinja2==3.1.0
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Install a package via `--extra-index-url`.
|
||||||
|
///
|
||||||
|
/// If the package exists exist on the "extra" index at a compatible version, the resolver should
|
||||||
|
/// prefer it, even if a newer versions exists on the "primary" index.
|
||||||
|
///
|
||||||
|
/// In this case, Jinja 3.1.2 is hosted on the "extra" index, but newer versions are available on
|
||||||
|
/// the "primary" index.
|
||||||
|
#[test]
|
||||||
|
fn compile_index_url_fallback_prefer_primary() -> Result<()> {
|
||||||
|
let context = TestContext::new("3.12");
|
||||||
|
|
||||||
|
let requirements_in = context.temp_dir.child("requirements.in");
|
||||||
|
requirements_in.write_str("jinja2")?;
|
||||||
|
|
||||||
|
uv_snapshot!(context.compile_without_exclude_newer()
|
||||||
|
.arg("--index-strategy")
|
||||||
|
.arg("unsafe-any-match")
|
||||||
|
.arg("--index-url")
|
||||||
|
.arg("https://pypi.org/simple")
|
||||||
|
.arg("--extra-index-url")
|
||||||
|
.arg("https://download.pytorch.org/whl/cpu")
|
||||||
|
.arg("requirements.in")
|
||||||
|
.arg("--no-deps"), @r###"
|
||||||
|
success: true
|
||||||
|
exit_code: 0
|
||||||
|
----- stdout -----
|
||||||
|
# This file was autogenerated by uv via the following command:
|
||||||
|
# uv pip compile --cache-dir [CACHE_DIR] --index-strategy unsafe-any-match requirements.in --no-deps
|
||||||
|
jinja2==3.1.2
|
||||||
|
|
||||||
|
----- stderr -----
|
||||||
|
Resolved 1 package in [TIME]
|
||||||
|
"###
|
||||||
|
);
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue