diff --git a/Cargo.lock b/Cargo.lock
index 9ee6e4b0889a..0b71c519c5f3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5121,6 +5121,7 @@ dependencies = [
"serde",
"thiserror",
"toml",
+ "uv-distribution-types",
"uv-pep440",
"uv-pep508",
"uv-pypi-types",
@@ -5285,6 +5286,7 @@ dependencies = [
"toml_edit",
"tracing",
"url",
+ "uv-distribution-types",
"uv-fs",
"uv-git",
"uv-macros",
diff --git a/crates/uv-cli/src/lib.rs b/crates/uv-cli/src/lib.rs
index 8cdbde832203..c173e4cef2e6 100644
--- a/crates/uv-cli/src/lib.rs
+++ b/crates/uv-cli/src/lib.rs
@@ -7,13 +7,14 @@ use anyhow::{anyhow, Result};
use clap::builder::styling::{AnsiColor, Effects, Style};
use clap::builder::Styles;
use clap::{Args, Parser, Subcommand};
+
use url::Url;
use uv_cache::CacheArgs;
use uv_configuration::{
ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier,
TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
};
-use uv_distribution_types::{FlatIndexLocation, IndexUrl};
+use uv_distribution_types::{FlatIndexLocation, Index, IndexUrl};
use uv_normalize::{ExtraName, PackageName};
use uv_pep508::Requirement;
use uv_pypi_types::VerbatimParsedUrl;
@@ -793,6 +794,36 @@ fn parse_flat_index(input: &str) -> Result, String> {
}
}
+/// Parse a string into an [`Index`], mapping the empty string to `None`.
+fn parse_index_source(input: &str) -> Result, String> {
+ if input.is_empty() {
+ Ok(Maybe::None)
+ } else {
+ match Index::from_str(input) {
+ Ok(index) => Ok(Maybe::Some(Index {
+ default: false,
+ ..index
+ })),
+ Err(err) => Err(err.to_string()),
+ }
+ }
+}
+
+/// Parse a string into an [`Index`], mapping the empty string to `None`.
+fn parse_default_index_source(input: &str) -> Result, String> {
+ if input.is_empty() {
+ Ok(Maybe::None)
+ } else {
+ match Index::from_str(input) {
+ Ok(index) => Ok(Maybe::Some(Index {
+ default: true,
+ ..index
+ })),
+ Err(err) => Err(err.to_string()),
+ }
+ }
+}
+
/// Parse a string into an [`Url`], mapping the empty string to `None`.
fn parse_insecure_host(input: &str) -> Result, String> {
if input.is_empty() {
@@ -2282,8 +2313,8 @@ pub struct VenvArgs {
///
/// 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 (`first-match`). This prevents
- /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the
- /// same name to a secondary.
+ /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the
+ /// same name to an alternate index.
#[arg(long, value_enum, env = EnvVars::UV_INDEX_STRATEGY)]
pub index_strategy: Option,
@@ -3808,7 +3839,28 @@ pub struct GenerateShellCompletionArgs {
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct IndexArgs {
- /// The URL of the Python package index (by default: ).
+ /// The URLs to use when resolving dependencies, in addition to the default index.
+ ///
+ /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local
+ /// directory laid out in the same format.
+ ///
+ /// All indexes provided via this flag take priority over the index specified by
+ /// `--default-index` (which defaults to PyPI). When multiple `--index` flags are
+ /// provided, earlier values take priority.
+ #[arg(long, env = "UV_INDEX", value_delimiter = ' ', value_parser = parse_index_source, help_heading = "Index options")]
+ pub index: Option>>,
+
+ /// The URL of the default package index (by default: ).
+ ///
+ /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local
+ /// directory laid out in the same format.
+ ///
+ /// The index given by this flag is given lower priority than all other indexes specified via
+ /// the `--index` flag.
+ #[arg(long, env = "UV_DEFAULT_INDEX", value_parser = parse_default_index_source, help_heading = "Index options")]
+ pub default_index: Option>,
+
+ /// (Deprecated: use `--default-index` instead) The URL of the Python package index (by default: ).
///
/// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local
/// directory laid out in the same format.
@@ -3818,7 +3870,7 @@ pub struct IndexArgs {
#[arg(long, short, env = EnvVars::UV_INDEX_URL, value_parser = parse_index_url, help_heading = "Index options")]
pub index_url: Option>,
- /// Extra URLs of package indexes to use, in addition to `--index-url`.
+ /// (Deprecated: use `--index` instead) Extra URLs of package indexes to use, in addition to `--index-url`.
///
/// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local
/// directory laid out in the same format.
@@ -3955,8 +4007,8 @@ pub struct InstallerArgs {
///
/// 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 (`first-match`). This prevents
- /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the
- /// same name to a secondary.
+ /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the
+ /// same name to an alternate index.
#[arg(
long,
value_enum,
@@ -4117,8 +4169,8 @@ pub struct ResolverArgs {
///
/// 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 (`first-match`). This prevents
- /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the
- /// same name to a secondary.
+ /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the
+ /// same name to an alternate index.
#[arg(
long,
value_enum,
@@ -4309,8 +4361,8 @@ pub struct ResolverInstallerArgs {
///
/// 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 (`first-match`). This prevents
- /// "dependency confusion" attacks, whereby an attack can upload a malicious package under the
- /// same name to a secondary.
+ /// "dependency confusion" attacks, whereby an attacker can upload a malicious package under the
+ /// same name to an alternate index.
#[arg(
long,
value_enum,
diff --git a/crates/uv-cli/src/options.rs b/crates/uv-cli/src/options.rs
index 3fe5d0e77ea4..499a2109a80d 100644
--- a/crates/uv-cli/src/options.rs
+++ b/crates/uv-cli/src/options.rs
@@ -1,7 +1,7 @@
use uv_cache::Refresh;
use uv_configuration::ConfigSettings;
use uv_resolver::PrereleaseMode;
-use uv_settings::{PipOptions, ResolverInstallerOptions, ResolverOptions};
+use uv_settings::{Combine, PipOptions, ResolverInstallerOptions, ResolverOptions};
use crate::{
BuildOptionsArgs, IndexArgs, InstallerArgs, Maybe, RefreshArgs, ResolverArgs,
@@ -186,6 +186,8 @@ impl From for PipOptions {
impl From for PipOptions {
fn from(args: IndexArgs) -> Self {
let IndexArgs {
+ default_index,
+ index,
index_url,
extra_index_url,
no_index,
@@ -193,6 +195,12 @@ impl From for PipOptions {
} = args;
Self {
+ index: default_index
+ .and_then(Maybe::into_option)
+ .map(|default_index| vec![default_index])
+ .combine(
+ index.map(|index| index.into_iter().filter_map(Maybe::into_option).collect()),
+ ),
index_url: index_url.and_then(Maybe::into_option),
extra_index_url: extra_index_url.map(|extra_index_url| {
extra_index_url
@@ -247,6 +255,15 @@ pub fn resolver_options(
} = build_args;
ResolverOptions {
+ index: index_args
+ .default_index
+ .and_then(Maybe::into_option)
+ .map(|default_index| vec![default_index])
+ .combine(
+ index_args
+ .index
+ .map(|index| index.into_iter().filter_map(Maybe::into_option).collect()),
+ ),
index_url: index_args.index_url.and_then(Maybe::into_option),
extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
extra_index_url
@@ -335,7 +352,16 @@ pub fn resolver_installer_options(
no_binary_package,
} = build_args;
+ let default_index = index_args
+ .default_index
+ .and_then(Maybe::into_option)
+ .map(|default_index| vec![default_index]);
+ let index = index_args
+ .index
+ .map(|index| index.into_iter().filter_map(Maybe::into_option).collect());
+
ResolverInstallerOptions {
+ index: default_index.combine(index),
index_url: index_args.index_url.and_then(Maybe::into_option),
extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
extra_index_url
diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs
index 923fa8a9a184..aee7ba7f8c64 100644
--- a/crates/uv-client/src/registry_client.rs
+++ b/crates/uv-client/src/registry_client.rs
@@ -1,6 +1,7 @@
use async_http_range_reader::AsyncHttpRangeReader;
use futures::{FutureExt, TryStreamExt};
use http::HeaderMap;
+use itertools::Either;
use reqwest::{Client, Response, StatusCode};
use reqwest_middleware::ClientWithMiddleware;
use std::collections::BTreeMap;
@@ -16,7 +17,7 @@ use uv_configuration::KeyringProviderType;
use uv_configuration::{IndexStrategy, TrustedHost};
use uv_distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
use uv_distribution_types::{
- BuiltDist, File, FileLocation, IndexCapabilities, IndexUrl, IndexUrls, Name,
+ BuiltDist, File, FileLocation, Index, IndexCapabilities, IndexUrl, IndexUrls, Name,
};
use uv_metadata::{read_metadata_async_seek, read_metadata_async_stream};
use uv_normalize::PackageName;
@@ -204,8 +205,15 @@ impl RegistryClient {
pub async fn simple(
&self,
package_name: &PackageName,
+ index: Option<&IndexUrl>,
) -> Result)>, Error> {
- let mut it = self.index_urls.indexes().peekable();
+ let indexes = if let Some(index) = index {
+ Either::Left(std::iter::once(index))
+ } else {
+ Either::Right(self.index_urls.indexes().map(Index::url))
+ };
+
+ let mut it = indexes.peekable();
if it.peek().is_none() {
return Err(ErrorKind::NoIndex(package_name.to_string()).into());
}
diff --git a/crates/uv-distribution-types/src/index.rs b/crates/uv-distribution-types/src/index.rs
new file mode 100644
index 000000000000..baa7d8175730
--- /dev/null
+++ b/crates/uv-distribution-types/src/index.rs
@@ -0,0 +1,146 @@
+use crate::{IndexUrl, IndexUrlError};
+use std::str::FromStr;
+use thiserror::Error;
+use url::Url;
+
+#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
+#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+pub struct Index {
+ /// The name of the index.
+ ///
+ /// Index names can be used to reference indexes elsewhere in the configuration. For example,
+ /// you can pin a package to a specific index by name:
+ ///
+ /// ```toml
+ /// [[tool.uv.index]]
+ /// name = "pytorch"
+ /// url = "https://download.pytorch.org/whl/cu121"
+ ///
+ /// [tool.uv.sources]
+ /// torch = { index = "pytorch" }
+ /// ```
+ pub name: Option,
+ /// The URL of the index.
+ ///
+ /// Expects to receive a URL (e.g., `https://pypi.org/simple`) or a local path.
+ pub url: IndexUrl,
+ /// Mark the index as explicit.
+ ///
+ /// Explicit indexes will _only_ be used when explicitly requested via a `[tool.uv.sources]`
+ /// definition, as in:
+ ///
+ /// ```toml
+ /// [[tool.uv.index]]
+ /// name = "pytorch"
+ /// url = "https://download.pytorch.org/whl/cu121"
+ /// explicit = true
+ ///
+ /// [tool.uv.sources]
+ /// torch = { index = "pytorch" }
+ /// ```
+ #[serde(default)]
+ pub explicit: bool,
+ /// Mark the index as the default index.
+ ///
+ /// By default, uv uses PyPI as the default index, such that even if additional indexes are
+ /// defined via `[[tool.uv.index]]`, PyPI will still be used as a fallback for packages that
+ /// aren't found elsewhere. To disable the PyPI default, set `default = true` on at least one
+ /// other index.
+ ///
+ /// Marking an index as default will move it to the front of the list of indexes, such that it
+ /// is given the highest priority when resolving packages.
+ #[serde(default)]
+ pub default: bool,
+ // /// The type of the index.
+ // ///
+ // /// Indexes can either be PEP 503-compliant (i.e., a registry implementing the Simple API) or
+ // /// structured as a flat list of distributions (e.g., `--find-links`). In both cases, indexes
+ // /// can point to either local or remote resources.
+ // #[serde(default)]
+ // pub r#type: IndexKind,
+}
+
+// #[derive(
+// Default, Debug, Copy, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize,
+// )]
+// #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
+// pub enum IndexKind {
+// /// A PEP 503 and/or PEP 691-compliant index.
+// #[default]
+// Simple,
+// /// An index containing a list of links to distributions (e.g., `--find-links`).
+// Flat,
+// }
+
+impl Index {
+ /// Initialize an [`Index`] from a pip-style `--index-url`.
+ pub fn from_index_url(url: IndexUrl) -> Self {
+ Self {
+ url,
+ name: None,
+ explicit: false,
+ default: true,
+ }
+ }
+
+ /// Initialize an [`Index`] from a pip-style `--extra-index-url`.
+ pub fn from_extra_index_url(url: IndexUrl) -> Self {
+ Self {
+ url,
+ name: None,
+ explicit: false,
+ default: false,
+ }
+ }
+
+ /// Return the [`IndexUrl`] of the index.
+ pub fn url(&self) -> &IndexUrl {
+ &self.url
+ }
+
+ /// Return the raw [`URL`] of the index.
+ pub fn raw_url(&self) -> &Url {
+ self.url.url()
+ }
+}
+
+impl FromStr for Index {
+ type Err = IndexSourceError;
+
+ fn from_str(s: &str) -> Result {
+ // Determine whether the source is prefixed with a name, as in `name=https://pypi.org/simple`.
+ if let Some((name, url)) = s.split_once('=') {
+ if name.is_empty() {
+ return Err(IndexSourceError::EmptyName);
+ }
+
+ if name.chars().all(char::is_alphanumeric) {
+ let url = IndexUrl::from_str(url)?;
+ return Ok(Self {
+ name: Some(name.to_string()),
+ url,
+ explicit: false,
+ default: false,
+ });
+ }
+ }
+
+ // Otherwise, assume the source is a URL.
+ let url = IndexUrl::from_str(s)?;
+ Ok(Self {
+ name: None,
+ url,
+ explicit: false,
+ default: false,
+ })
+ }
+}
+
+/// An error that can occur when parsing an [`Index`].
+#[derive(Error, Debug)]
+pub enum IndexSourceError {
+ #[error(transparent)]
+ Url(#[from] IndexUrlError),
+ #[error("Index included a name, but the name was empty")]
+ EmptyName,
+}
diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs
index cd3eb8ad146a..f868f8c83528 100644
--- a/crates/uv-distribution-types/src/index_url.rs
+++ b/crates/uv-distribution-types/src/index_url.rs
@@ -11,12 +11,13 @@ use url::{ParseError, Url};
use uv_pep508::{VerbatimUrl, VerbatimUrlError};
-use crate::Verbatim;
+use crate::{Index, Verbatim};
static PYPI_URL: LazyLock = LazyLock::new(|| Url::parse("https://pypi.org/simple").unwrap());
-static DEFAULT_INDEX_URL: LazyLock =
- LazyLock::new(|| IndexUrl::Pypi(VerbatimUrl::from_url(PYPI_URL.clone())));
+static DEFAULT_INDEX_URL: LazyLock = LazyLock::new(|| {
+ Index::from_index_url(IndexUrl::Pypi(VerbatimUrl::from_url(PYPI_URL.clone())))
+});
/// The URL of an index to use for fetching packages (e.g., PyPI).
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
@@ -55,6 +56,15 @@ impl IndexUrl {
}
}
+ /// Convert the index URL into a [`Url`].
+ pub fn into_url(self) -> Url {
+ match self {
+ Self::Pypi(url) => url.into_url(),
+ Self::Url(url) => url.into_url(),
+ Self::Path(url) => url.into_url(),
+ }
+ }
+
/// Return the redacted URL for the index, omitting any sensitive credentials.
pub fn redacted(&self) -> Cow<'_, Url> {
let url = self.url();
@@ -288,39 +298,21 @@ impl From for FlatIndexLocation {
/// The index locations to use for fetching packages. By default, uses the PyPI index.
///
-/// From a pip perspective, this type merges `--index-url`, `--extra-index-url`, and `--find-links`.
-#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
+/// This type merges the legacy `--index-url`, `--extra-index-url`, and `--find-links` options,
+/// along with the uv-specific `--index` and `--default-index`.
+#[derive(Default, Debug, Clone, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct IndexLocations {
- index: Option,
- extra_index: Vec,
+ indexes: Vec,
flat_index: Vec,
no_index: bool,
}
-impl Default for IndexLocations {
- /// By default, use the `PyPI` index.
- fn default() -> Self {
- Self {
- index: Some(DEFAULT_INDEX_URL.clone()),
- extra_index: Vec::new(),
- flat_index: Vec::new(),
- no_index: false,
- }
- }
-}
-
impl IndexLocations {
/// Determine the index URLs to use for fetching packages.
- pub fn new(
- index: Option,
- extra_index: Vec,
- flat_index: Vec,
- no_index: bool,
- ) -> Self {
+ pub fn new(indexes: Vec, flat_index: Vec, no_index: bool) -> Self {
Self {
- index,
- extra_index,
+ indexes,
flat_index,
no_index,
}
@@ -335,14 +327,12 @@ impl IndexLocations {
#[must_use]
pub fn combine(
self,
- index: Option,
- extra_index: Vec,
+ indexes: Vec,
flat_index: Vec,
no_index: bool,
) -> Self {
Self {
- index: self.index.or(index),
- extra_index: self.extra_index.into_iter().chain(extra_index).collect(),
+ indexes: self.indexes.into_iter().chain(indexes).collect(),
flat_index: self.flat_index.into_iter().chain(flat_index).collect(),
no_index: self.no_index || no_index,
}
@@ -351,47 +341,69 @@ impl IndexLocations {
/// Returns `true` if no index configuration is set, i.e., the [`IndexLocations`] matches the
/// default configuration.
pub fn is_none(&self) -> bool {
- self.index.is_none()
- && self.extra_index.is_empty()
- && self.flat_index.is_empty()
- && !self.no_index
+ *self == Self::default()
}
}
impl<'a> IndexLocations {
- /// Return the primary [`IndexUrl`] entry.
+ /// Return the default [`Index`] entry.
///
/// If `--no-index` is set, return `None`.
///
/// If no index is provided, use the `PyPI` index.
- pub fn index(&'a self) -> Option<&'a IndexUrl> {
+ pub fn default_index(&'a self) -> Option<&'a Index> {
if self.no_index {
None
} else {
- match self.index.as_ref() {
- Some(index) => Some(index),
- None => Some(&DEFAULT_INDEX_URL),
- }
+ let mut seen = FxHashSet::default();
+ self.indexes
+ .iter()
+ .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
+ .find(|index| index.default && !index.explicit)
+ .or_else(|| Some(&DEFAULT_INDEX_URL))
}
}
- /// Return an iterator over the extra [`IndexUrl`] entries.
- pub fn extra_index(&'a self) -> impl Iterator + 'a {
+ /// Return an iterator over the implicit [`Index`] entries.
+ pub fn implicit_indexes(&'a self) -> impl Iterator + 'a {
if self.no_index {
Either::Left(std::iter::empty())
} else {
- Either::Right(self.extra_index.iter())
+ let mut seen = FxHashSet::default();
+ Either::Right(
+ self.indexes
+ .iter()
+ .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
+ .filter(|index| !(index.default || index.explicit)),
+ )
}
}
- /// Return an iterator over all [`IndexUrl`] entries in order.
+ /// Return an iterator over the explicit [`Index`] entries.
+ pub fn explicit_indexes(&'a self) -> impl Iterator + 'a {
+ if self.no_index {
+ Either::Left(std::iter::empty())
+ } else {
+ let mut seen = FxHashSet::default();
+ Either::Right(
+ self.indexes
+ .iter()
+ .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
+ .filter(|index| index.explicit),
+ )
+ }
+ }
+
+ /// Return an iterator over all [`Index`] entries in order.
+ ///
+ /// Explicit indexes are excluded.
///
- /// Prioritizes the extra indexes over the main index.
+ /// Prioritizes the extra indexes over the default index.
///
/// If `no_index` was enabled, then this always returns an empty
/// iterator.
- pub fn indexes(&'a self) -> impl Iterator + 'a {
- self.extra_index().chain(self.index())
+ pub fn indexes(&'a self) -> impl Iterator + 'a {
+ self.implicit_indexes().chain(self.default_index())
}
/// Return an iterator over the [`FlatIndexLocation`] entries.
@@ -407,88 +419,89 @@ impl<'a> IndexLocations {
/// Clone the index locations into a [`IndexUrls`] instance.
pub fn index_urls(&'a self) -> IndexUrls {
IndexUrls {
- index: self.index.clone(),
- extra_index: self.extra_index.clone(),
+ indexes: self.indexes.clone(),
no_index: self.no_index,
}
}
- /// Return an iterator over all [`Url`] entries.
- pub fn urls(&'a self) -> impl Iterator + 'a {
- self.indexes()
- .map(IndexUrl::url)
- .chain(self.flat_index.iter().filter_map(|index| match index {
- FlatIndexLocation::Path(_) => None,
- FlatIndexLocation::Url(url) => Some(url.raw()),
- }))
+ /// Return an iterator over all allowed [`IndexUrl`] entries.
+ ///
+ /// This includes both explicit and implicit indexes, as well as the default index.
+ ///
+ /// If `no_index` was enabled, then this always returns an empty
+ /// iterator.
+ pub fn allowed_indexes(&'a self) -> impl Iterator + 'a {
+ self.explicit_indexes()
+ .chain(self.implicit_indexes())
+ .chain(self.default_index())
+ }
+
+ /// Return an iterator over all allowed [`Url`] entries.
+ ///
+ /// This includes both explicit and implicit index URLs, as well as the default index.
+ ///
+ /// If `no_index` was enabled, then this always returns an empty
+ /// iterator.
+ pub fn allowed_urls(&'a self) -> impl Iterator + 'a {
+ self.allowed_indexes()
+ .map(Index::raw_url)
+ .chain(self.flat_index().map(FlatIndexLocation::url))
}
}
/// The index URLs to use for fetching packages.
///
-/// From a pip perspective, this type merges `--index-url` and `--extra-index-url`.
-#[derive(Debug, Clone)]
+/// This type merges the legacy `--index-url` and `--extra-index-url` options, along with the
+/// uv-specific `--index` and `--default-index`.
+#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct IndexUrls {
- index: Option,
- extra_index: Vec,
+ indexes: Vec,
no_index: bool,
}
-impl Default for IndexUrls {
- /// By default, use the `PyPI` index.
- fn default() -> Self {
- Self {
- index: Some(DEFAULT_INDEX_URL.clone()),
- extra_index: Vec::new(),
- no_index: false,
- }
- }
-}
-
impl<'a> IndexUrls {
- /// Return the fallback [`IndexUrl`] entry.
+ /// Return the default [`Index`] entry.
///
/// If `--no-index` is set, return `None`.
///
/// If no index is provided, use the `PyPI` index.
- fn index(&'a self) -> Option<&'a IndexUrl> {
+ fn default_index(&'a self) -> Option<&'a Index> {
if self.no_index {
None
} else {
- match self.index.as_ref() {
- Some(index) => Some(index),
- None => Some(&DEFAULT_INDEX_URL),
- }
+ let mut seen = FxHashSet::default();
+ self.indexes
+ .iter()
+ .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
+ .find(|index| index.default && !index.explicit)
+ .or_else(|| Some(&DEFAULT_INDEX_URL))
}
}
- /// Return an iterator over the extra [`IndexUrl`] entries.
- fn extra_index(&'a self) -> impl Iterator + 'a {
+ /// Return an iterator over the implicit [`Index`] entries.
+ fn implicit_indexes(&'a self) -> impl Iterator + 'a {
if self.no_index {
Either::Left(std::iter::empty())
} else {
- Either::Right(self.extra_index.iter())
+ let mut seen = FxHashSet::default();
+ Either::Right(
+ self.indexes
+ .iter()
+ .filter(move |index| index.name.as_ref().map_or(true, |name| seen.insert(name)))
+ .filter(|index| !(index.default || index.explicit)),
+ )
}
}
/// Return an iterator over all [`IndexUrl`] entries in order.
///
- /// Prioritizes the extra indexes over the main index.
+ /// Prioritizes the `[tool.uv.index]` definitions over the `--extra-index-url` definitions
+ /// over the `--index-url` definition.
///
/// If `no_index` was enabled, then this always returns an empty
/// iterator.
- pub fn indexes(&'a self) -> impl Iterator + 'a {
- self.extra_index().chain(self.index())
- }
-}
-
-impl From for IndexUrls {
- fn from(locations: IndexLocations) -> Self {
- Self {
- index: locations.index,
- extra_index: locations.extra_index,
- no_index: locations.no_index,
- }
+ pub fn indexes(&'a self) -> impl Iterator + 'a {
+ self.implicit_indexes().chain(self.default_index())
}
}
diff --git a/crates/uv-distribution-types/src/lib.rs b/crates/uv-distribution-types/src/lib.rs
index a77d0518544e..3758284a19e6 100644
--- a/crates/uv-distribution-types/src/lib.rs
+++ b/crates/uv-distribution-types/src/lib.rs
@@ -57,6 +57,7 @@ pub use crate::error::*;
pub use crate::file::*;
pub use crate::hash::*;
pub use crate::id::*;
+pub use crate::index::*;
pub use crate::index_url::*;
pub use crate::installed::*;
pub use crate::prioritized_distribution::*;
@@ -75,6 +76,7 @@ mod error;
mod file;
mod hash;
mod id;
+mod index;
mod index_url;
mod installed;
mod prioritized_distribution;
diff --git a/crates/uv-distribution-types/src/resolution.rs b/crates/uv-distribution-types/src/resolution.rs
index f8b51b160e6e..6ec6edce3be0 100644
--- a/crates/uv-distribution-types/src/resolution.rs
+++ b/crates/uv-distribution-types/src/resolution.rs
@@ -190,7 +190,7 @@ impl From<&ResolvedDist> for Requirement {
wheels.best_wheel().filename.version.clone(),
),
),
- index: None,
+ index: Some(wheels.best_wheel().index.url().clone()),
},
Dist::Built(BuiltDist::DirectUrl(wheel)) => {
let mut location = wheel.url.to_url();
@@ -211,7 +211,7 @@ impl From<&ResolvedDist> for Requirement {
specifier: uv_pep440::VersionSpecifiers::from(
uv_pep440::VersionSpecifier::equals_version(sdist.version.clone()),
),
- index: None,
+ index: Some(sdist.index.url().clone()),
},
Dist::Source(SourceDist::DirectUrl(sdist)) => {
let mut location = sdist.url.to_url();
diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs
index f751a3fea403..5646e91e3e1f 100644
--- a/crates/uv-distribution/src/index/registry_wheel_index.rs
+++ b/crates/uv-distribution/src/index/registry_wheel_index.rs
@@ -3,7 +3,7 @@ use std::collections::hash_map::Entry;
use rustc_hash::FxHashMap;
use uv_cache::{Cache, CacheBucket, WheelCache};
-use uv_distribution_types::{CachedRegistryDist, Hashed, IndexLocations, IndexUrl};
+use uv_distribution_types::{CachedRegistryDist, Hashed, Index, IndexLocations, IndexUrl};
use uv_fs::{directories, files, symlinks};
use uv_normalize::PackageName;
use uv_platform_tags::Tags;
@@ -17,6 +17,8 @@ use crate::source::{HttpRevisionPointer, LocalRevisionPointer, HTTP_REVISION, LO
pub struct IndexEntry {
/// The cached distribution.
pub dist: CachedRegistryDist,
+ /// The index from which the wheel was downloaded.
+ pub index: Index,
/// Whether the wheel was built from source (true), or downloaded from the registry directly (false).
pub built: bool,
}
@@ -80,23 +82,22 @@ impl<'a> RegistryWheelIndex<'a> {
) -> Vec {
let mut entries = vec![];
- // Collect into owned `IndexUrl`.
- let flat_index_urls: Vec = index_locations
+ let flat_index_urls: Vec = index_locations
.flat_index()
- .map(|flat_index| IndexUrl::from(flat_index.clone()))
+ .map(|flat_index| Index::from_extra_index_url(IndexUrl::from(flat_index.clone())))
.collect();
- for index_url in index_locations.indexes().chain(flat_index_urls.iter()) {
+ for index in index_locations.indexes().chain(flat_index_urls.iter()) {
// Index all the wheels that were downloaded directly from the registry.
let wheel_dir = cache.shard(
CacheBucket::Wheels,
- WheelCache::Index(index_url).wheel_dir(package.to_string()),
+ WheelCache::Index(index.url()).wheel_dir(package.to_string()),
);
// For registry wheels, the cache structure is: `//.http`
// or `///.rev`.
for file in files(&wheel_dir) {
- match index_url {
+ match index.url() {
// Add files from remote registries.
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
if file
@@ -116,6 +117,7 @@ impl<'a> RegistryWheelIndex<'a> {
) {
entries.push(IndexEntry {
dist: wheel.into_registry_dist(),
+ index: index.clone(),
built: false,
});
}
@@ -142,6 +144,7 @@ impl<'a> RegistryWheelIndex<'a> {
) {
entries.push(IndexEntry {
dist: wheel.into_registry_dist(),
+ index: index.clone(),
built: false,
});
}
@@ -156,7 +159,7 @@ impl<'a> RegistryWheelIndex<'a> {
// from the registry.
let cache_shard = cache.shard(
CacheBucket::SourceDistributions,
- WheelCache::Index(index_url).wheel_dir(package.to_string()),
+ WheelCache::Index(index.url()).wheel_dir(package.to_string()),
);
// For registry wheels, the cache structure is: `///`.
@@ -165,7 +168,7 @@ impl<'a> RegistryWheelIndex<'a> {
let cache_shard = cache_shard.shard(shard);
// Read the revision from the cache.
- let revision = match index_url {
+ let revision = match index.url() {
// Add files from remote registries.
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
let revision_entry = cache_shard.entry(HTTP_REVISION);
@@ -197,6 +200,7 @@ impl<'a> RegistryWheelIndex<'a> {
) {
entries.push(IndexEntry {
dist: wheel.into_registry_dist(),
+ index: index.clone(),
built: true,
});
}
diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs
index 9ad9207f80fb..5f3480eab6a6 100644
--- a/crates/uv-distribution/src/metadata/lowering.rs
+++ b/crates/uv-distribution/src/metadata/lowering.rs
@@ -6,6 +6,7 @@ use thiserror::Error;
use url::Url;
use uv_distribution_filename::DistExtension;
+use uv_distribution_types::Index;
use uv_git::GitReference;
use uv_normalize::PackageName;
use uv_pep440::VersionSpecifiers;
@@ -33,6 +34,7 @@ impl LoweredRequirement {
project_name: &'data PackageName,
project_dir: &'data Path,
project_sources: &'data BTreeMap,
+ project_indexes: &'data [Index],
workspace: &'data Workspace,
lower_bound: LowerBound,
) -> impl Iterator> + 'data {
@@ -151,7 +153,26 @@ impl LoweredRequirement {
(source, marker)
}
Source::Registry { index, marker } => {
- let source = registry_source(&requirement, index)?;
+ // Identify the named index from either the project indexes or the workspace indexes,
+ // in that order.
+ let Some(index) = project_indexes
+ .iter()
+ .find(|Index { name, .. }| {
+ name.as_ref().is_some_and(|name| *name == index)
+ })
+ .or_else(|| {
+ workspace.indexes().iter().find(|Index { name, .. }| {
+ name.as_ref().is_some_and(|name| *name == index)
+ })
+ })
+ .map(|Index { url: index, .. }| index.clone())
+ else {
+ return Err(LoweringError::MissingIndex(
+ requirement.name.clone(),
+ index,
+ ));
+ };
+ let source = registry_source(&requirement, index.into_url())?;
(source, marker)
}
Source::Workspace {
@@ -238,6 +259,7 @@ impl LoweredRequirement {
requirement: uv_pep508::Requirement,
dir: &'data Path,
sources: &'data BTreeMap,
+ indexes: &'data [Index],
) -> impl Iterator> + 'data {
let source = sources.get(&requirement.name).cloned();
@@ -318,7 +340,19 @@ impl LoweredRequirement {
(source, marker)
}
Source::Registry { index, marker } => {
- let source = registry_source(&requirement, index)?;
+ let Some(index) = indexes
+ .iter()
+ .find(|Index { name, .. }| {
+ name.as_ref().is_some_and(|name| *name == index)
+ })
+ .map(|Index { url: index, .. }| index.clone())
+ else {
+ return Err(LoweringError::MissingIndex(
+ requirement.name.clone(),
+ index,
+ ));
+ };
+ let source = registry_source(&requirement, index.into_url())?;
(source, marker)
}
Source::Workspace { .. } => {
@@ -358,6 +392,8 @@ pub enum LoweringError {
UndeclaredWorkspacePackage,
#[error("Can only specify one of: `rev`, `tag`, or `branch`")]
MoreThanOneGitRef,
+ #[error("Package `{0}` references an undeclared index: `{1}`")]
+ MissingIndex(PackageName, String),
#[error("Workspace members are not allowed in non-workspace contexts")]
WorkspaceMember,
#[error(transparent)]
@@ -448,7 +484,7 @@ fn url_source(url: Url, subdirectory: Option) -> Result,
- index: String,
+ index: Url,
) -> Result {
match &requirement.version_or_url {
None => {
diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs
index 5e67c846e934..2df77967a237 100644
--- a/crates/uv-distribution/src/metadata/requires_dist.rs
+++ b/crates/uv-distribution/src/metadata/requires_dist.rs
@@ -59,6 +59,20 @@ impl RequiresDist {
source_strategy: SourceStrategy,
lower_bound: LowerBound,
) -> Result {
+ // Collect any `tool.uv.index` entries.
+ let empty = vec![];
+ let indexes = match source_strategy {
+ SourceStrategy::Enabled => project_workspace
+ .current_project()
+ .pyproject_toml()
+ .tool
+ .as_ref()
+ .and_then(|tool| tool.uv.as_ref())
+ .and_then(|uv| uv.index.as_deref())
+ .unwrap_or(&empty),
+ SourceStrategy::Disabled => &empty,
+ };
+
// Collect any `tool.uv.sources` and `tool.uv.dev_dependencies` from `pyproject.toml`.
let empty = BTreeMap::default();
let sources = match source_strategy {
@@ -94,6 +108,7 @@ impl RequiresDist {
&metadata.name,
project_workspace.project_root(),
sources,
+ indexes,
project_workspace.workspace(),
lower_bound,
)
@@ -127,6 +142,7 @@ impl RequiresDist {
&metadata.name,
project_workspace.project_root(),
sources,
+ indexes,
project_workspace.workspace(),
lower_bound,
)
diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs
index b1241355d8a1..cd654206ce30 100644
--- a/crates/uv-installer/src/plan.rs
+++ b/crates/uv-installer/src/plan.rs
@@ -121,6 +121,9 @@ impl<'a> Planner<'a> {
match installable {
Dist::Built(BuiltDist::Registry(wheel)) => {
if let Some(distribution) = registry_index.get(wheel.name()).find_map(|entry| {
+ if *entry.index.url() != wheel.best_wheel().index {
+ return None;
+ }
if entry.dist.filename.version != wheel.best_wheel().filename.version {
return None;
};
@@ -231,6 +234,9 @@ impl<'a> Planner<'a> {
}
Dist::Source(SourceDist::Registry(sdist)) => {
if let Some(distribution) = registry_index.get(sdist.name()).find_map(|entry| {
+ if *entry.index.url() != sdist.index {
+ return None;
+ }
if entry.dist.filename.version != sdist.version {
return None;
};
diff --git a/crates/uv-pep508/src/verbatim_url.rs b/crates/uv-pep508/src/verbatim_url.rs
index e3224c99e9fc..f75539d2ed2b 100644
--- a/crates/uv-pep508/src/verbatim_url.rs
+++ b/crates/uv-pep508/src/verbatim_url.rs
@@ -134,6 +134,11 @@ impl VerbatimUrl {
self.url.clone()
}
+ /// Convert a [`VerbatimUrl`] into a [`Url`].
+ pub fn into_url(self) -> Url {
+ self.url
+ }
+
/// Return the underlying [`Path`], if the URL is a file URL.
pub fn as_path(&self) -> Result {
self.url
diff --git a/crates/uv-pypi-types/src/requirement.rs b/crates/uv-pypi-types/src/requirement.rs
index ee930c573353..101b943f36b5 100644
--- a/crates/uv-pypi-types/src/requirement.rs
+++ b/crates/uv-pypi-types/src/requirement.rs
@@ -318,8 +318,8 @@ pub enum RequirementSource {
/// The requirement has a version specifier, such as `foo >1,<2`.
Registry {
specifier: VersionSpecifiers,
- /// Choose a version from the index with this name.
- index: Option,
+ /// Choose a version from the index at the given URL.
+ index: Option,
},
// TODO(konsti): Track and verify version specifier from `project.dependencies` matches the
// version in remote location.
@@ -607,7 +607,7 @@ enum RequirementSourceWire {
Registry {
#[serde(skip_serializing_if = "VersionSpecifiers::is_empty", default)]
specifier: VersionSpecifiers,
- index: Option,
+ index: Option,
},
}
diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs
index e16caabcdf03..786e3b6e3c4e 100644
--- a/crates/uv-resolver/src/error.rs
+++ b/crates/uv-resolver/src/error.rs
@@ -53,6 +53,9 @@ pub enum ResolveError {
fork_markers: MarkerTree,
},
+ #[error("Requirements contain conflicting indexes for package `{0}`: `{1}` vs. `{2}`")]
+ ConflictingIndexes(PackageName, String, String),
+
#[error("Package `{0}` attempted to resolve via URL: {1}. URL dependencies must be expressed as direct requirements or constraints. Consider adding `{0} @ {1}` to your dependencies or constraints file.")]
DisallowedUrl(PackageName, String),
diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs
index 1c4de1cd09d0..2e9d18801123 100644
--- a/crates/uv-resolver/src/lock/mod.rs
+++ b/crates/uv-resolver/src/lock/mod.rs
@@ -1058,10 +1058,10 @@ impl Lock {
// Collect the set of available indexes (both `--index-url` and `--find-links` entries).
let remotes = indexes.map(|locations| {
locations
- .indexes()
- .filter_map(|index_url| match index_url {
+ .allowed_indexes()
+ .filter_map(|index| match index.url() {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
- Some(UrlString::from(index_url.redacted()))
+ Some(UrlString::from(index.url().redacted()))
}
IndexUrl::Path(_) => None,
})
@@ -1080,11 +1080,11 @@ impl Lock {
let locals = indexes.map(|locations| {
locations
- .indexes()
- .filter_map(|index_url| match index_url {
+ .allowed_indexes()
+ .filter_map(|index| match index.url() {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
- IndexUrl::Path(index_url) => {
- let path = index_url.to_file_path().ok()?;
+ IndexUrl::Path(url) => {
+ let path = url.to_file_path().ok()?;
let path = relative_to(&path, workspace.install_path())
.or_else(|_| std::path::absolute(path))
.ok()?;
diff --git a/crates/uv-resolver/src/pubgrub/report.rs b/crates/uv-resolver/src/pubgrub/report.rs
index 688206318bbe..150b7c653b2b 100644
--- a/crates/uv-resolver/src/pubgrub/report.rs
+++ b/crates/uv-resolver/src/pubgrub/report.rs
@@ -9,7 +9,7 @@ use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Te
use rustc_hash::FxHashMap;
use uv_configuration::IndexStrategy;
-use uv_distribution_types::{IndexLocations, IndexUrl};
+use uv_distribution_types::{Index, IndexLocations, IndexUrl};
use uv_normalize::PackageName;
use uv_pep440::Version;
@@ -708,6 +708,7 @@ impl PubGrubReportFormatter<'_> {
// indexes were not queried, and could contain a compatible version.
if let Some(next_index) = index_locations
.indexes()
+ .map(Index::url)
.skip_while(|url| *url != found_index)
.nth(1)
{
diff --git a/crates/uv-resolver/src/resolver/indexes.rs b/crates/uv-resolver/src/resolver/indexes.rs
new file mode 100644
index 000000000000..d925cd786274
--- /dev/null
+++ b/crates/uv-resolver/src/resolver/indexes.rs
@@ -0,0 +1,67 @@
+use crate::{DependencyMode, Manifest, ResolveError, ResolverMarkers};
+
+use rustc_hash::FxHashMap;
+use std::collections::hash_map::Entry;
+use uv_distribution_types::IndexUrl;
+use uv_normalize::PackageName;
+use uv_pep508::VerbatimUrl;
+use uv_pypi_types::RequirementSource;
+
+/// A map of package names to their explicit index.
+///
+/// For example, given:
+/// ```toml
+/// [[tool.uv.index]]
+/// name = "pytorch"
+/// url = "https://download.pytorch.org/whl/cu121"
+///
+/// [tool.uv.sources]
+/// torch = { index = "pytorch" }
+/// ```
+///
+/// [`Indexes`] would contain a single entry mapping `torch` to `https://download.pytorch.org/whl/cu121`.
+#[derive(Debug, Default, Clone)]
+pub(crate) struct Indexes(FxHashMap);
+
+impl Indexes {
+ /// Determine the set of explicit, pinned indexes in the [`Manifest`].
+ pub(crate) fn from_manifest(
+ manifest: &Manifest,
+ markers: &ResolverMarkers,
+ dependencies: DependencyMode,
+ ) -> Result {
+ let mut indexes = FxHashMap::::default();
+
+ for requirement in manifest.requirements(markers, dependencies) {
+ let RequirementSource::Registry {
+ index: Some(index), ..
+ } = &requirement.source
+ else {
+ continue;
+ };
+ let index = IndexUrl::from(VerbatimUrl::from_url(index.clone()));
+ match indexes.entry(requirement.name.clone()) {
+ Entry::Occupied(entry) => {
+ let existing = entry.get();
+ if *existing != index {
+ return Err(ResolveError::ConflictingIndexes(
+ requirement.name.clone(),
+ existing.to_string(),
+ index.to_string(),
+ ));
+ }
+ }
+ Entry::Vacant(entry) => {
+ entry.insert(index);
+ }
+ }
+ }
+
+ Ok(Self(indexes))
+ }
+
+ /// Return the explicit index for a given [`PackageName`].
+ pub(crate) fn get(&self, package_name: &PackageName) -> Option<&IndexUrl> {
+ self.0.get(package_name)
+ }
+}
diff --git a/crates/uv-resolver/src/resolver/mod.rs b/crates/uv-resolver/src/resolver/mod.rs
index 9050d2b9ae8b..1bc06f5c5668 100644
--- a/crates/uv-resolver/src/resolver/mod.rs
+++ b/crates/uv-resolver/src/resolver/mod.rs
@@ -28,8 +28,9 @@ use uv_configuration::{Constraints, Overrides};
use uv_distribution::{ArchiveMetadata, DistributionDatabase};
use uv_distribution_types::{
BuiltDist, CompatibleDist, Dist, DistributionMetadata, IncompatibleDist, IncompatibleSource,
- IncompatibleWheel, IndexCapabilities, IndexLocations, InstalledDist, PythonRequirementKind,
- RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist, VersionOrUrlRef,
+ IncompatibleWheel, IndexCapabilities, IndexLocations, IndexUrl, InstalledDist,
+ PythonRequirementKind, RemoteSource, ResolvedDist, ResolvedDistRef, SourceDist,
+ VersionOrUrlRef,
};
use uv_git::GitResolver;
use uv_normalize::{ExtraName, GroupName, PackageName};
@@ -60,6 +61,7 @@ pub(crate) use crate::resolver::availability::{
use crate::resolver::batch_prefetch::BatchPrefetcher;
use crate::resolver::groups::Groups;
pub use crate::resolver::index::InMemoryIndex;
+use crate::resolver::indexes::Indexes;
pub use crate::resolver::provider::{
DefaultResolverProvider, MetadataResponse, PackageVersionsResult, ResolverProvider,
VersionsResponse, WheelMetadataResult,
@@ -74,6 +76,7 @@ mod batch_prefetch;
mod fork_map;
mod groups;
mod index;
+mod indexes;
mod locals;
mod provider;
mod reporter;
@@ -100,6 +103,7 @@ struct ResolverState {
exclusions: Exclusions,
urls: Urls,
locals: Locals,
+ indexes: Indexes,
dependency_mode: DependencyMode,
hasher: HashStrategy,
markers: ResolverMarkers,
@@ -204,6 +208,7 @@ impl
dependency_mode: options.dependency_mode,
urls: Urls::from_manifest(&manifest, &markers, git, options.dependency_mode)?,
locals: Locals::from_manifest(&manifest, &markers, options.dependency_mode),
+ indexes: Indexes::from_manifest(&manifest, &markers, options.dependency_mode)?,
groups: Groups::from_manifest(&manifest, &markers),
project: manifest.project,
workspace_members: manifest.workspace_members,
@@ -377,7 +382,9 @@ impl ResolverState ResolverState ResolverState ResolverState ResolverState,
+ index: Option<&IndexUrl>,
request_sink: &Sender,
) -> Result<(), ResolveError> {
// Ignore unresolved URL packages.
@@ -732,13 +742,14 @@ impl ResolverState,
+ index: Option<&IndexUrl>,
request_sink: &Sender,
) -> Result<(), ResolveError> {
// Only request real package
@@ -760,7 +771,7 @@ impl ResolverState ResolverState Result
May also be set with the UV_CONSTRAINT environment variable.
+
--default-indexdefault-index
The URL of the default package index (by default: <https://pypi.org/simple>).
+
+
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
+
+
The index given by this flag is given lower priority than all other indexes specified via the --index flag.
+
+
May also be set with the UV_DEFAULT_INDEX environment variable.
--directorydirectory
Change to the given directory prior to running the command.
Relative paths are resolved with the given directory as the base.
@@ -5461,7 +5636,7 @@ uv pip install [OPTIONS] |--editable Only applies to pyproject.toml, setup.py, and setup.cfg sources.
-
--extra-index-urlextra-index-url
Extra URLs of package indexes to use, in addition to --index-url.
+
--extra-index-urlextra-index-url
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -5477,9 +5652,16 @@ uv pip install [OPTIONS] |--editable May also be set with the UV_FIND_LINKS environment variable.
--help, -h
Display the concise help for this command
+
--indexindex
The URLs to use when resolving dependencies, in addition to the default index.
+
+
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
+
+
All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.
+
+
May also be set with the UV_INDEX environment variable.
--index-strategyindex-strategy
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 (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.
+
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 (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.
May also be set with the UV_INDEX_STRATEGY environment variable.
Possible values:
@@ -5491,7 +5673,7 @@ uv pip install [OPTIONS] |--editable unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
-
--index-url, -iindex-url
The URL of the Python package index (by default: <https://pypi.org/simple>).
+
--index-url, -iindex-url
(Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -6620,6 +6802,13 @@ uv venv [OPTIONS] [PATH]
While uv configuration can be included in a pyproject.toml file, it is not allowed in this context.
May also be set with the UV_CONFIG_FILE environment variable.
+
--default-indexdefault-index
The URL of the default package index (by default: <https://pypi.org/simple>).
+
+
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
+
+
The index given by this flag is given lower priority than all other indexes specified via the --index flag.
+
+
May also be set with the UV_DEFAULT_INDEX environment variable.
--directorydirectory
Change to the given directory prior to running the command.
Relative paths are resolved with the given directory as the base.
@@ -6631,7 +6820,7 @@ uv venv [OPTIONS] [PATH]
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.
May also be set with the UV_EXCLUDE_NEWER environment variable.
-
--extra-index-urlextra-index-url
Extra URLs of package indexes to use, in addition to --index-url.
+
--extra-index-urlextra-index-url
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -6647,9 +6836,16 @@ uv venv [OPTIONS] [PATH]
May also be set with the UV_FIND_LINKS environment variable.
--help, -h
Display the concise help for this command
+
--indexindex
The URLs to use when resolving dependencies, in addition to the default index.
+
+
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
+
+
All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.
+
+
May also be set with the UV_INDEX environment variable.
--index-strategyindex-strategy
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 (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.
+
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 (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.
May also be set with the UV_INDEX_STRATEGY environment variable.
Possible values:
@@ -6661,7 +6857,7 @@ uv venv [OPTIONS] [PATH]
unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
-
--index-url, -iindex-url
The URL of the Python package index (by default: <https://pypi.org/simple>).
+
--index-url, -iindex-url
(Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -6868,6 +7064,13 @@ uv build [OPTIONS] [SRC]
May also be set with the UV_CONFIG_FILE environment variable.
--config-setting, -Cconfig-setting
Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs
+
--default-indexdefault-index
The URL of the default package index (by default: <https://pypi.org/simple>).
+
+
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
+
+
The index given by this flag is given lower priority than all other indexes specified via the --index flag.
+
+
May also be set with the UV_DEFAULT_INDEX environment variable.
--directorydirectory
Change to the given directory prior to running the command.
Relative paths are resolved with the given directory as the base.
@@ -6879,7 +7082,7 @@ uv build [OPTIONS] [SRC]
Accepts both RFC 3339 timestamps (e.g., 2006-12-02T02:07:43Z) and local dates in the same format (e.g., 2006-12-02) in your system’s configured time zone.
May also be set with the UV_EXCLUDE_NEWER environment variable.
-
--extra-index-urlextra-index-url
Extra URLs of package indexes to use, in addition to --index-url.
+
--extra-index-urlextra-index-url
(Deprecated: use --index instead) Extra URLs of package indexes to use, in addition to --index-url.
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
@@ -6895,9 +7098,16 @@ uv build [OPTIONS] [SRC]
May also be set with the UV_FIND_LINKS environment variable.
--help, -h
Display the concise help for this command
+
--indexindex
The URLs to use when resolving dependencies, in addition to the default index.
+
+
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
+
+
All indexes provided via this flag take priority over the index specified by --default-index (which defaults to PyPI). When multiple --index flags are provided, earlier values take priority.
+
+
May also be set with the UV_INDEX environment variable.
--index-strategyindex-strategy
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 (first-match). This prevents "dependency confusion" attacks, whereby an attack can upload a malicious package under the same name to a secondary.
+
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 (first-match). This prevents "dependency confusion" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.
May also be set with the UV_INDEX_STRATEGY environment variable.
Possible values:
@@ -6909,7 +7119,7 @@ uv build [OPTIONS] [SRC]
unsafe-best-match: Search for every package name across all indexes, preferring the "best" version found. If a package version is in multiple indexes, only look at the entry for the first index
-
--index-url, -iindex-url
The URL of the Python package index (by default: <https://pypi.org/simple>).
+
--index-url, -iindex-url
(Deprecated: use --default-index instead) The URL of the Python package index (by default: <https://pypi.org/simple>).
Accepts either a repository compliant with PEP 503 (the simple repository API), or a local directory laid out in the same format.
diff --git a/docs/reference/settings.md b/docs/reference/settings.md
index 4e05ccb40aa9..ff89d4e8be8f 100644
--- a/docs/reference/settings.md
+++ b/docs/reference/settings.md
@@ -73,6 +73,51 @@ environments = ["sys_platform == 'darwin'"]
---
+### [`index`](#index) {: #index }
+
+The indexes to use when resolving dependencies.
+
+Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)
+(the simple repository API), or a local directory laid out in the same format.
+
+Indexes are considered in the order in which they're defined, such that the first-defined
+index has the highest priority. Further, the indexes provided by this setting are given
+higher priority than any indexes specified via [`index_url`](#index-url) or
+[`extra_index_url`](#extra-index-url). uv will only consider the first index that contains
+a given package, unless an alternative [index strategy](#index-strategy) is specified.
+
+If an index is marked as `explicit = true`, it will be used exclusively for those
+dependencies that select it explicitly via `[tool.uv.sources]`, as in:
+
+```toml
+[[tool.uv.index]]
+name = "pytorch"
+url = "https://download.pytorch.org/whl/cu121"
+explicit = true
+
+[tool.uv.sources]
+torch = { index = "pytorch" }
+```
+
+If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is
+given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
+PyPI default index.
+
+**Default value**: `"[]"`
+
+**Type**: `dict`
+
+**Example usage**:
+
+```toml title="pyproject.toml"
+[tool.uv]
+[[tool.uv.index]]
+name = "pytorch"
+url = "https://download.pytorch.org/whl/cu121"
+```
+
+---
+
### [`managed`](#managed) {: #managed }
Whether the project is managed by uv. If `false`, uv will ignore the project when
@@ -533,11 +578,14 @@ Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep
(the simple repository API), or a local directory laid out in the same format.
All indexes provided via this flag take priority over the index specified by
-[`index_url`](#index-url). When multiple indexes are provided, earlier values take priority.
+[`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes
+are provided, earlier values take priority.
To control uv's resolution strategy when multiple indexes are present, see
[`index_strategy`](#index-strategy).
+(Deprecated: use `index` instead.)
+
**Default value**: `[]`
**Type**: `list[str]`
@@ -591,14 +639,69 @@ formats described above.
---
+### [`index`](#index) {: #index }
+
+The package indexes to use when resolving dependencies.
+
+Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/)
+(the simple repository API), or a local directory laid out in the same format.
+
+Indexes are considered in the order in which they're defined, such that the first-defined
+index has the highest priority. Further, the indexes provided by this setting are given
+higher priority than any indexes specified via [`index_url`](#index-url) or
+[`extra_index_url`](#extra-index-url). uv will only consider the first index that contains
+a given package, unless an alternative [index strategy](#index-strategy) is specified.
+
+If an index is marked as `explicit = true`, it will be used exclusively for those
+dependencies that select it explicitly via `[tool.uv.sources]`, as in:
+
+```toml
+[[tool.uv.index]]
+name = "pytorch"
+url = "https://download.pytorch.org/whl/cu121"
+explicit = true
+
+[tool.uv.sources]
+torch = { index = "pytorch" }
+```
+
+If an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is
+given the lowest priority when resolving packages. Additionally, marking an index as default will disable the
+PyPI default index.
+
+**Default value**: `"[]"`
+
+**Type**: `dict`
+
+**Example usage**:
+
+=== "pyproject.toml"
+
+ ```toml
+ [tool.uv]
+ [[tool.uv.index]]
+ name = "pytorch"
+ url = "https://download.pytorch.org/whl/cu121"
+ ```
+=== "uv.toml"
+
+ ```toml
+
+ [[tool.uv.index]]
+ name = "pytorch"
+ url = "https://download.pytorch.org/whl/cu121"
+ ```
+
+---
+
### [`index-strategy`](#index-strategy) {: #index-strategy }
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 (`first-match`). This prevents
-"dependency confusion" attacks, whereby an attack can upload a malicious package under the
-same name to a secondary.
+"dependency confusion" attacks, whereby an attacker can upload a malicious package under the
+same name to an alternate index.
**Default value**: `"first-index"`
@@ -633,7 +736,9 @@ Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep
(the simple repository API), or a local directory laid out in the same format.
The index provided by this setting is given lower priority than any indexes specified via
-[`extra_index_url`](#extra-index-url).
+[`extra_index_url`](#extra-index-url) or [`index`](#index).
+
+(Deprecated: use `index` instead.)
**Default value**: `"https://pypi.org/simple"`
@@ -1911,8 +2016,8 @@ 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 (`first-match`). This prevents
-"dependency confusion" attacks, whereby an attack can upload a malicious package under the
-same name to a secondary.
+"dependency confusion" attacks, whereby an attacker can upload a malicious package under the
+same name to an alternate index.
**Default value**: `"first-index"`
diff --git a/mkdocs.template.yml b/mkdocs.template.yml
index 158ce2cd6970..eff7a4058937 100644
--- a/mkdocs.template.yml
+++ b/mkdocs.template.yml
@@ -106,6 +106,7 @@ nav:
- Configuration files: configuration/files.md
- Environment variables: configuration/environment.md
- Authentication: configuration/authentication.md
+ - Package indexes: configuration/indexes.md
- Integration guides:
- guides/integration/index.md
- Docker: guides/integration/docker.md
diff --git a/uv.schema.json b/uv.schema.json
index ced2bb33b3f2..f6ab2f8c57b4 100644
--- a/uv.schema.json
+++ b/uv.schema.json
@@ -128,7 +128,7 @@
]
},
"extra-index-url": {
- "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by [`index_url`](#index-url). When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see [`index_strategy`](#index-strategy).",
+ "description": "Extra URLs of package indexes to use, in addition to `--index-url`.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nAll indexes provided via this flag take priority over the index specified by [`index_url`](#index-url) or [`index`](#index) with `default = true`. When multiple indexes are provided, earlier values take priority.\n\nTo control uv's resolution strategy when multiple indexes are present, see [`index_strategy`](#index-strategy).\n\n(Deprecated: use `index` instead.)",
"type": [
"array",
"null"
@@ -147,8 +147,18 @@
"$ref": "#/definitions/FlatIndexLocation"
}
},
+ "index": {
+ "description": "The indexes to use when resolving dependencies.\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nIndexes are considered in the order in which they're defined, such that the first-defined index has the highest priority. Further, the indexes provided by this setting are given higher priority than any indexes specified via [`index_url`](#index-url) or [`extra_index_url`](#extra-index-url). uv will only consider the first index that contains a given package, unless an alternative [index strategy](#index-strategy) is specified.\n\nIf an index is marked as `explicit = true`, it will be used exclusively for those dependencies that select it explicitly via `[tool.uv.sources]`, as in:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\" explicit = true\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```\n\nIf an index is marked as `default = true`, it will be moved to the end of the prioritized list, such that it is given the lowest priority when resolving packages. Additionally, marking an index as default will disable the PyPI default index.",
+ "type": [
+ "array",
+ "null"
+ ],
+ "items": {
+ "$ref": "#/definitions/Index"
+ }
+ },
"index-strategy": {
- "description": "The strategy to use when resolving against multiple index URLs.\n\nBy 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 (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attack can upload a malicious package under the same name to a secondary.",
+ "description": "The strategy to use when resolving against multiple index URLs.\n\nBy 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 (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.",
"anyOf": [
{
"$ref": "#/definitions/IndexStrategy"
@@ -159,7 +169,7 @@
]
},
"index-url": {
- "description": "The URL of the Python package index (by default: ).\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nThe index provided by this setting is given lower priority than any indexes specified via [`extra_index_url`](#extra-index-url).",
+ "description": "The URL of the Python package index (by default: ).\n\nAccepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) (the simple repository API), or a local directory laid out in the same format.\n\nThe index provided by this setting is given lower priority than any indexes specified via [`extra_index_url`](#extra-index-url) or [`index`](#index).\n\n(Deprecated: use `index` instead.)",
"anyOf": [
{
"$ref": "#/definitions/IndexUrl"
@@ -545,6 +555,39 @@
"description": "The path to a directory of distributions, or a URL to an HTML file with a flat listing of distributions.",
"type": "string"
},
+ "Index": {
+ "type": "object",
+ "required": [
+ "url"
+ ],
+ "properties": {
+ "default": {
+ "description": "Mark the index as the default index.\n\nBy default, uv uses PyPI as the default index, such that even if additional indexes are defined via `[[tool.uv.index]]`, PyPI will still be used as a fallback for packages that aren't found elsewhere. To disable the PyPI default, set `default = true` on at least one other index.\n\nMarking an index as default will move it to the front of the list of indexes, such that it is given the highest priority when resolving packages.",
+ "default": false,
+ "type": "boolean"
+ },
+ "explicit": {
+ "description": "Mark the index as explicit.\n\nExplicit indexes will _only_ be used when explicitly requested via a `[tool.uv.sources]` definition, as in:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\" explicit = true\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```",
+ "default": false,
+ "type": "boolean"
+ },
+ "name": {
+ "description": "The name of the index.\n\nIndex names can be used to reference indexes elsewhere in the configuration. For example, you can pin a package to a specific index by name:\n\n```toml [[tool.uv.index]] name = \"pytorch\" url = \"https://download.pytorch.org/whl/cu121\"\n\n[tool.uv.sources] torch = { index = \"pytorch\" } ```",
+ "type": [
+ "string",
+ "null"
+ ]
+ },
+ "url": {
+ "description": "The URL of the index.\n\nExpects to receive a URL (e.g., `https://pypi.org/simple`) or a local path.",
+ "allOf": [
+ {
+ "$ref": "#/definitions/IndexUrl"
+ }
+ ]
+ }
+ }
+ },
"IndexStrategy": {
"oneOf": [
{
@@ -803,7 +846,7 @@
]
},
"index-strategy": {
- "description": "The strategy to use when resolving against multiple index URLs.\n\nBy 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 (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attack can upload a malicious package under the same name to a secondary.",
+ "description": "The strategy to use when resolving against multiple index URLs.\n\nBy 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 (`first-match`). This prevents \"dependency confusion\" attacks, whereby an attacker can upload a malicious package under the same name to an alternate index.",
"anyOf": [
{
"$ref": "#/definitions/IndexStrategy"