From f5aa09641490f90c7d8a1ef2cfe5bc8438d41f94 Mon Sep 17 00:00:00 2001 From: Charlie Marsh Date: Tue, 17 Sep 2024 15:30:26 -0400 Subject: [PATCH] Add explicit index support --- Cargo.lock | 3 + crates/distribution-types/src/index.rs | 146 ++ crates/distribution-types/src/index_url.rs | 203 +- crates/distribution-types/src/lib.rs | 2 + crates/distribution-types/src/resolution.rs | 4 +- crates/pep508-rs/src/verbatim_url.rs | 5 + crates/pypi-types/src/requirement.rs | 4 +- crates/uv-cli/src/lib.rs | 73 +- crates/uv-cli/src/options.rs | 28 +- crates/uv-client/src/registry_client.rs | 12 +- crates/uv-distribution/Cargo.toml | 1 + .../src/index/registry_wheel_index.rs | 65 +- .../uv-distribution/src/metadata/lowering.rs | 36 +- .../src/metadata/requires_dist.rs | 16 + crates/uv-installer/src/plan.rs | 24 +- crates/uv-resolver/src/error.rs | 3 + crates/uv-resolver/src/lock/mod.rs | 14 +- crates/uv-resolver/src/pubgrub/report.rs | 3 +- crates/uv-resolver/src/resolver/indexes.rs | 65 + crates/uv-resolver/src/resolver/mod.rs | 33 +- crates/uv-resolver/src/resolver/provider.rs | 9 +- crates/uv-scripts/Cargo.toml | 1 + crates/uv-scripts/src/lib.rs | 7 +- crates/uv-settings/src/settings.rs | 92 +- crates/uv-workspace/Cargo.toml | 3 +- crates/uv-workspace/src/pyproject.rs | 54 +- crates/uv-workspace/src/workspace.rs | 50 +- crates/uv/src/commands/build.rs | 2 +- crates/uv/src/commands/pip/compile.rs | 23 +- crates/uv/src/commands/pip/install.rs | 15 +- crates/uv/src/commands/pip/sync.rs | 15 +- crates/uv/src/commands/project/add.rs | 2 +- crates/uv/src/commands/project/lock.rs | 2 +- crates/uv/src/commands/project/mod.rs | 18 +- crates/uv/src/commands/project/run.rs | 16 +- crates/uv/src/commands/project/sync.rs | 2 +- crates/uv/src/commands/venv.rs | 4 +- crates/uv/src/settings.rs | 58 +- crates/uv/tests/lock.rs | 441 +++- crates/uv/tests/pip_install.rs | 2 +- crates/uv/tests/show_settings.rs | 1851 +++++++++++++---- crates/uv/tests/sync.rs | 57 + docs/concepts/dependencies.md | 24 + docs/configuration/environment.md | 7 +- docs/configuration/index.md | 1 + docs/configuration/indexes.md | 114 + docs/pip/compatibility.md | 7 +- docs/reference/cli.md | 300 ++- docs/reference/settings.md | 113 +- mkdocs.template.yml | 1 + uv.schema.json | 51 +- 51 files changed, 3446 insertions(+), 636 deletions(-) create mode 100644 crates/distribution-types/src/index.rs create mode 100644 crates/uv-resolver/src/resolver/indexes.rs create mode 100644 docs/configuration/indexes.md diff --git a/Cargo.lock b/Cargo.lock index c40627e5bbba..e998c8604d46 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4803,6 +4803,7 @@ dependencies = [ "distribution-types", "fs-err", "futures", + "indexmap", "indoc", "insta", "nanoid", @@ -5184,6 +5185,7 @@ dependencies = [ name = "uv-scripts" version = "0.0.1" dependencies = [ + "distribution-types", "fs-err", "indoc", "memchr", @@ -5328,6 +5330,7 @@ version = "0.0.1" dependencies = [ "anyhow", "assert_fs", + "distribution-types", "either", "fs-err", "glob", diff --git a/crates/distribution-types/src/index.rs b/crates/distribution-types/src/index.rs new file mode 100644 index 000000000000..4d335a8f7302 --- /dev/null +++ b/crates/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 enabled 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/distribution-types/src/index_url.rs b/crates/distribution-types/src/index_url.rs index 19175c608c61..c6dcc0ea0ee6 100644 --- a/crates/distribution-types/src/index_url.rs +++ b/crates/distribution-types/src/index_url.rs @@ -11,12 +11,13 @@ use url::{ParseError, Url}; use pep508_rs::{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)] +/// From a pip perspective, this type merges `--index-url`, `--extra-index-url`, and `--find-links`, +/// along with the uv-specific `--index` and `--default-index` options. +#[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)] +/// From a pip perspective, this type merges `--index-url` and `--extra-index-url`, along with the +/// uv-specific `--index` and `--default-index` options. +#[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/distribution-types/src/lib.rs b/crates/distribution-types/src/lib.rs index 7c48fb38e050..c5f3abddcd1c 100644 --- a/crates/distribution-types/src/lib.rs +++ b/crates/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/distribution-types/src/resolution.rs b/crates/distribution-types/src/resolution.rs index 8501d4a30855..cfe62b3ba1c3 100644 --- a/crates/distribution-types/src/resolution.rs +++ b/crates/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: pep440_rs::VersionSpecifiers::from( pep440_rs::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/pep508-rs/src/verbatim_url.rs b/crates/pep508-rs/src/verbatim_url.rs index 9445b13cfd78..d48b979515ab 100644 --- a/crates/pep508-rs/src/verbatim_url.rs +++ b/crates/pep508-rs/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/pypi-types/src/requirement.rs b/crates/pypi-types/src/requirement.rs index 2d1854e2fbe8..d600e33c9a91 100644 --- a/crates/pypi-types/src/requirement.rs +++ b/crates/pypi-types/src/requirement.rs @@ -319,7 +319,7 @@ pub enum RequirementSource { Registry { specifier: VersionSpecifiers, /// Choose a version from the index with this name. - index: Option, + 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-cli/src/lib.rs b/crates/uv-cli/src/lib.rs index c57522ac47ba..074da2589dd2 100644 --- a/crates/uv-cli/src/lib.rs +++ b/crates/uv-cli/src/lib.rs @@ -7,7 +7,7 @@ use anyhow::{anyhow, Result}; use clap::builder::styling::{AnsiColor, Effects, Style}; use clap::builder::Styles; use clap::{Args, Parser, Subcommand}; -use distribution_types::{FlatIndexLocation, IndexUrl}; +use distribution_types::{FlatIndexLocation, Index, IndexUrl}; use pep508_rs::Requirement; use pypi_types::VerbatimParsedUrl; use url::Url; @@ -779,6 +779,36 @@ fn parse_index_url(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() { @@ -2236,8 +2266,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 = "UV_INDEX_STRATEGY")] pub index_strategy: Option, @@ -3694,6 +3724,27 @@ pub struct GenerateShellCompletionArgs { #[derive(Args)] #[allow(clippy::struct_excessive_bools)] pub struct IndexArgs { + /// 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>, + /// The URL of the Python package index (by default: ). /// /// Accepts either a repository compliant with PEP 503 (the simple repository API), or a local @@ -3701,6 +3752,8 @@ pub struct IndexArgs { /// /// The index given by this flag is given lower priority than all other /// indexes specified via the `--extra-index-url` flag. + /// + /// (Deprecated: use `--default-index` instead.) #[arg(long, short, env = "UV_INDEX_URL", value_parser = parse_index_url, help_heading = "Index options")] pub index_url: Option>, @@ -3712,6 +3765,8 @@ pub struct IndexArgs { /// All indexes provided via this flag take priority over the index specified by /// `--index-url` (which defaults to PyPI). When multiple `--extra-index-url` flags are /// provided, earlier values take priority. + /// + /// (Deprecated: use `--index` instead.) #[arg(long, env = "UV_EXTRA_INDEX_URL", value_delimiter = ' ', value_parser = parse_index_url, help_heading = "Index options")] pub extra_index_url: Option>>, @@ -3835,8 +3890,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, @@ -3997,8 +4052,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, @@ -4189,8 +4244,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 fb7cd5235c58..6a99f42c7727 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_urls| { extra_index_urls @@ -242,6 +250,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_urls| { extra_index_urls @@ -325,7 +342,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_urls| { extra_index_urls diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 392e17647cf5..57b7f1d61586 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -6,6 +6,7 @@ use std::str::FromStr; 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 tracing::{info_span, instrument, trace, warn, Instrument}; @@ -13,7 +14,7 @@ use url::Url; use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename}; use distribution_types::{ - BuiltDist, File, FileLocation, IndexCapabilities, IndexUrl, IndexUrls, Name, + BuiltDist, File, FileLocation, Index, IndexCapabilities, IndexUrl, IndexUrls, Name, }; use pep440_rs::Version; use pep508_rs::MarkerEnvironment; @@ -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/Cargo.toml b/crates/uv-distribution/Cargo.toml index 1845a60951fd..23fec1cbbb8f 100644 --- a/crates/uv-distribution/Cargo.toml +++ b/crates/uv-distribution/Cargo.toml @@ -36,6 +36,7 @@ uv-workspace = { workspace = true } anyhow = { workspace = true } fs-err = { workspace = true } futures = { workspace = true } +indexmap = { workspace = true } nanoid = { workspace = true } owo-colors = { workspace = true } reqwest = { workspace = true } diff --git a/crates/uv-distribution/src/index/registry_wheel_index.rs b/crates/uv-distribution/src/index/registry_wheel_index.rs index 38a8373458b9..1e3e1d7e226d 100644 --- a/crates/uv-distribution/src/index/registry_wheel_index.rs +++ b/crates/uv-distribution/src/index/registry_wheel_index.rs @@ -1,9 +1,9 @@ +use indexmap::IndexMap; +use rustc_hash::FxHashMap; use std::collections::hash_map::Entry; use std::collections::BTreeMap; -use rustc_hash::FxHashMap; - -use distribution_types::{CachedRegistryDist, Hashed, IndexLocations, IndexUrl}; +use distribution_types::{CachedRegistryDist, Hashed, Index, IndexLocations, IndexUrl}; use pep440_rs::Version; use platform_tags::Tags; use uv_cache::{Cache, CacheBucket, WheelCache}; @@ -21,7 +21,11 @@ pub struct RegistryWheelIndex<'a> { tags: &'a Tags, index_locations: &'a IndexLocations, hasher: &'a HashStrategy, - index: FxHashMap<&'a PackageName, BTreeMap>, + /// The cached distributions, indexed by package name and index. + /// + /// Index priority is respected, such that if a version is found in multiple indexes, the + /// highest priority index is + index: FxHashMap<&'a PackageName, IndexMap>>, } impl<'a> RegistryWheelIndex<'a> { @@ -47,24 +51,20 @@ impl<'a> RegistryWheelIndex<'a> { pub fn get( &mut self, name: &'a PackageName, - ) -> impl Iterator { - self.get_impl(name).iter().rev() + ) -> impl Iterator { + self.get_impl(name).iter().flat_map(|(index, versions)| { + versions + .iter() + .map(move |(version, dist)| (index, version, dist)) + }) } - /// Get the best wheel for the given package name and version. - /// - /// If the package is not yet indexed, this will index the package by reading from the cache. - pub fn get_version( + /// Get an entry in the index. + fn get_impl( &mut self, name: &'a PackageName, - version: &Version, - ) -> Option<&CachedRegistryDist> { - self.get_impl(name).get(version) - } - - /// Get an entry in the index. - fn get_impl(&mut self, name: &'a PackageName) -> &BTreeMap { - let versions = match self.index.entry(name) { + ) -> &IndexMap> { + let by_index = match self.index.entry(name) { Entry::Occupied(entry) => entry.into_mut(), Entry::Vacant(entry) => entry.insert(Self::index( name, @@ -74,7 +74,7 @@ impl<'a> RegistryWheelIndex<'a> { self.hasher, )), }; - versions + by_index } /// Add a package to the index by reading from the cache. @@ -84,26 +84,31 @@ impl<'a> RegistryWheelIndex<'a> { tags: &Tags, index_locations: &IndexLocations, hasher: &HashStrategy, - ) -> BTreeMap { - let mut versions = BTreeMap::new(); + ) -> IndexMap> { + let mut map = IndexMap::new(); // 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 + .allowed_indexes() + .chain(flat_index_urls.iter()) + { + let mut versions = BTreeMap::new(); + // 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 @@ -149,7 +154,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: `///`. @@ -158,7 +163,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); @@ -192,9 +197,11 @@ impl<'a> RegistryWheelIndex<'a> { } } } + + map.insert(index.clone(), versions); } - versions + map } /// Add the [`CachedWheel`] to the index. diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 378277546222..4d7fa401ea67 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 distribution_filename::DistExtension; +use distribution_types::Index; use pep440_rs::VersionSpecifiers; use pep508_rs::{VerbatimUrl, VersionOrUrl}; use pypi_types::{ParsedUrlError, Requirement, RequirementSource, VerbatimParsedUrl}; @@ -33,6 +34,7 @@ impl LoweredRequirement { project_name: &PackageName, project_dir: &Path, project_sources: &BTreeMap, + project_indexes: &[Index], workspace: &Workspace, ) -> Result { let (source, origin) = if let Some(source) = project_sources.get(&requirement.name) { @@ -108,7 +110,23 @@ impl LoweredRequirement { editable.unwrap_or(false), )? } - Source::Registry { index } => registry_source(&requirement, index)?, + Source::Registry { 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, index)); + }; + registry_source(&requirement, index.into_url())? + } Source::Workspace { workspace: is_workspace, } => { @@ -185,6 +203,7 @@ impl LoweredRequirement { requirement: pep508_rs::Requirement, dir: &Path, sources: &BTreeMap, + indexes: &[Index], ) -> Result { let source = sources.get(&requirement.name).cloned(); @@ -223,7 +242,16 @@ impl LoweredRequirement { editable.unwrap_or(false), )? } - Source::Registry { index } => registry_source(&requirement, index)?, + Source::Registry { 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, index)); + }; + registry_source(&requirement, index.into_url())? + } Source::Workspace { .. } => { return Err(LoweringError::WorkspaceMember); } @@ -258,6 +286,8 @@ pub enum LoweringError { MoreThanOneGitRef, #[error("Unable to combine options in `tool.uv.sources`")] InvalidEntry, + #[error("Package `{0}` references an undeclared index: `{1}`")] + MissingIndex(PackageName, String), #[error("Workspace members are not allowed in non-workspace contexts")] WorkspaceMember, #[error(transparent)] @@ -348,7 +378,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 c2b89103e74d..a7990c605b89 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -56,6 +56,20 @@ impl RequiresDist { project_workspace: &ProjectWorkspace, source_strategy: SourceStrategy, ) -> 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 { @@ -91,6 +105,7 @@ impl RequiresDist { &metadata.name, project_workspace.project_root(), sources, + indexes, project_workspace.workspace(), ) .map(LoweredRequirement::into_inner) @@ -119,6 +134,7 @@ impl RequiresDist { &metadata.name, project_workspace.project_root(), sources, + indexes, project_workspace.workspace(), ) .map(LoweredRequirement::into_inner) diff --git a/crates/uv-installer/src/plan.rs b/crates/uv-installer/src/plan.rs index f6562d906462..3e2831f76c9a 100644 --- a/crates/uv-installer/src/plan.rs +++ b/crates/uv-installer/src/plan.rs @@ -142,10 +142,28 @@ impl<'a> Planner<'a> { // Identify any cached distributions that satisfy the requirement. match &requirement.source { - RequirementSource::Registry { specifier, .. } => { - if let Some((_version, distribution)) = registry_index + RequirementSource::Registry { + specifier, + index: Some(url), + } => { + if let Some((_index, _version, distribution)) = registry_index + .get(&requirement.name) + .filter(|(index, _, _)| *index.raw_url() == *url) + .find(|(_index, version, _)| specifier.contains(version)) + { + debug!("Requirement already cached: {distribution}"); + cached.push(CachedDist::Registry(distribution.clone())); + continue; + } + } + RequirementSource::Registry { + specifier, + index: None, + } => { + if let Some((_index, _version, distribution)) = registry_index .get(&requirement.name) - .find(|(version, _)| specifier.contains(version)) + .filter(|(index, _, _)| !index.explicit) + .find(|(_, version, _)| specifier.contains(version)) { debug!("Requirement already cached: {distribution}"); cached.push(CachedDist::Registry(distribution.clone())); diff --git a/crates/uv-resolver/src/error.rs b/crates/uv-resolver/src/error.rs index b5e5bdfc9f41..b6bc323ddf7e 100644 --- a/crates/uv-resolver/src/error.rs +++ b/crates/uv-resolver/src/error.rs @@ -52,6 +52,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 06a0f03d36af..a238c16b3f1c 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1046,10 +1046,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, }) @@ -1068,11 +1068,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 f272020bcbb5..b4922cb9331f 100644 --- a/crates/uv-resolver/src/pubgrub/report.rs +++ b/crates/uv-resolver/src/pubgrub/report.rs @@ -8,7 +8,7 @@ use owo_colors::OwoColorize; use pubgrub::{DerivationTree, Derived, External, Map, Range, ReportFormatter, Term}; use rustc_hash::FxHashMap; -use distribution_types::{IndexLocations, IndexUrl}; +use distribution_types::{Index, IndexLocations, IndexUrl}; use pep440_rs::Version; use uv_configuration::IndexStrategy; use uv_normalize::PackageName; @@ -703,6 +703,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..1a0b94c2b3ec --- /dev/null +++ b/crates/uv-resolver/src/resolver/indexes.rs @@ -0,0 +1,65 @@ +use crate::{DependencyMode, Manifest, ResolveError, ResolverMarkers}; +use distribution_types::IndexUrl; +use pep508_rs::{PackageName, VerbatimUrl}; +use pypi_types::RequirementSource; +use rustc_hash::FxHashMap; +use std::collections::hash_map::Entry; + +/// 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 29c96fe23b4a..3952e02005c6 100644 --- a/crates/uv-resolver/src/resolver/mod.rs +++ b/crates/uv-resolver/src/resolver/mod.rs @@ -22,8 +22,9 @@ use tracing::{debug, info, instrument, trace, warn, Level}; use 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, }; pub(crate) use fork_map::{ForkMap, ForkSet}; use locals::Locals; @@ -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; @@ -99,6 +102,7 @@ struct ResolverState { exclusions: Exclusions, urls: Urls, locals: Locals, + indexes: Indexes, dependency_mode: DependencyMode, hasher: HashStrategy, markers: ResolverMarkers, @@ -201,6 +205,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, @@ -376,7 +381,9 @@ impl ResolverState ResolverState ResolverState ResolverState ResolverState, + index: Option<&IndexUrl>, request_sink: &Sender, ) -> Result<(), ResolveError> { // Ignore unresolved URL packages. @@ -731,13 +741,14 @@ impl ResolverState, + index: Option<&IndexUrl>, request_sink: &Sender, ) -> Result<(), ResolveError> { // Only request real package @@ -759,7 +770,7 @@ impl ResolverState ResolverState Result, ResolveError> { match request { // Fetch package metadata from the registry. - Request::Package(package_name) => { + Request::Package(package_name, index) => { let package_versions = provider - .get_package_versions(&package_name) + .get_package_versions(&package_name, index.as_ref()) .boxed_local() .await .map_err(ResolveError::Client)?; @@ -2503,7 +2514,7 @@ impl ResolutionPackage { #[allow(clippy::large_enum_variant)] pub(crate) enum Request { /// A request to fetch the metadata for a package. - Package(PackageName), + Package(PackageName, Option), /// A request to fetch the metadata for a built or source distribution. Dist(Dist), /// A request to fetch the metadata from an already-installed distribution. @@ -2552,7 +2563,7 @@ impl<'a> From> for Request { impl Display for Request { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self { - Self::Package(package_name) => { + Self::Package(package_name, _) => { write!(f, "Versions {package_name}") } Self::Dist(dist) => { diff --git a/crates/uv-resolver/src/resolver/provider.rs b/crates/uv-resolver/src/resolver/provider.rs index f4385ad79881..857c77724536 100644 --- a/crates/uv-resolver/src/resolver/provider.rs +++ b/crates/uv-resolver/src/resolver/provider.rs @@ -1,7 +1,6 @@ -use std::future::Future; - -use distribution_types::{Dist, IndexLocations}; +use distribution_types::{Dist, IndexLocations, IndexUrl}; use platform_tags::Tags; +use std::future::Future; use uv_configuration::BuildOptions; use uv_distribution::{ArchiveMetadata, DistributionDatabase}; use uv_normalize::PackageName; @@ -49,6 +48,7 @@ pub trait ResolverProvider { fn get_package_versions<'io>( &'io self, package_name: &'io PackageName, + index: Option<&'io IndexUrl>, ) -> impl Future + 'io; /// Get the metadata for a distribution. @@ -114,11 +114,12 @@ impl<'a, Context: BuildContext> ResolverProvider for DefaultResolverProvider<'a, async fn get_package_versions<'io>( &'io self, package_name: &'io PackageName, + index: Option<&'io IndexUrl>, ) -> PackageVersionsResult { let result = self .fetcher .client() - .managed(|client| client.simple(package_name)) + .managed(|client| client.simple(package_name, index)) .await; match result { diff --git a/crates/uv-scripts/Cargo.toml b/crates/uv-scripts/Cargo.toml index cf2265d493ad..4d9c5f2ea225 100644 --- a/crates/uv-scripts/Cargo.toml +++ b/crates/uv-scripts/Cargo.toml @@ -8,6 +8,7 @@ description = "Parse PEP 723-style Python scripts." workspace = true [dependencies] +distribution-types = { workspace = true } pep440_rs = { workspace = true } pep508_rs = { workspace = true } pypi-types = { workspace = true } diff --git a/crates/uv-scripts/src/lib.rs b/crates/uv-scripts/src/lib.rs index f257eb3f71c8..2ca7fd4773eb 100644 --- a/crates/uv-scripts/src/lib.rs +++ b/crates/uv-scripts/src/lib.rs @@ -4,13 +4,13 @@ use std::path::{Path, PathBuf}; use std::str::FromStr; use std::sync::LazyLock; +use distribution_types::Index; use memchr::memmem::Finder; -use serde::Deserialize; -use thiserror::Error; - use pep440_rs::VersionSpecifiers; use pep508_rs::PackageName; use pypi_types::VerbatimParsedUrl; +use serde::Deserialize; +use thiserror::Error; use uv_settings::{GlobalOptions, ResolverInstallerOptions}; use uv_workspace::pyproject::Source; @@ -194,6 +194,7 @@ pub struct ToolUv { #[serde(flatten)] pub top_level: ResolverInstallerOptions, pub sources: Option>, + pub indexes: Option>, } #[derive(Debug, Error)] diff --git a/crates/uv-settings/src/settings.rs b/crates/uv-settings/src/settings.rs index acd8fa5a9032..2881b840f3ac 100644 --- a/crates/uv-settings/src/settings.rs +++ b/crates/uv-settings/src/settings.rs @@ -1,6 +1,6 @@ use std::{fmt::Debug, num::NonZeroUsize, path::PathBuf}; -use distribution_types::{FlatIndexLocation, IndexUrl, StaticMetadata}; +use distribution_types::{FlatIndexLocation, Index, IndexUrl, StaticMetadata}; use install_wheel_rs::linker::LinkMode; use pep508_rs::Requirement; use pypi_types::{SupportedEnvironments, VerbatimParsedUrl}; @@ -234,6 +234,7 @@ pub struct GlobalOptions { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct InstallerOptions { + pub index: Option>, pub index_url: Option, pub extra_index_url: Option>, pub no_index: Option, @@ -261,6 +262,7 @@ pub struct InstallerOptions { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ResolverOptions { + pub index: Option>, pub index_url: Option, pub extra_index_url: Option>, pub no_index: Option, @@ -294,13 +296,49 @@ pub struct ResolverOptions { #[serde(rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ResolverInstallerOptions { + /// 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. + /// + /// 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" } + /// ``` + /// + /// Marking an index as `default = true` will disable the PyPI default index and move the + /// index to the end of the prioritized list, such that it is used when a package is not found + /// on any other index. + #[option( + default = "\"[]\"", + value_type = "dict", + example = r#" + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "# + )] + pub index: Option>, /// The URL of the Python package index (by default: ). /// /// 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. /// /// 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.) #[option( default = "\"https://pypi.org/simple\"", value_type = "str", @@ -315,10 +353,13 @@ pub struct ResolverInstallerOptions { /// (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.) #[option( default = "[]", value_type = "list[str]", @@ -357,8 +398,8 @@ pub struct ResolverInstallerOptions { /// /// 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. #[option( default = "\"first-index\"", value_type = "str", @@ -704,6 +745,35 @@ pub struct PipOptions { "# )] pub prefix: Option, + /// 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). + /// + /// 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 front of the list of + /// the list of indexes, such that it is given the highest priority when resolving packages. + /// Additionally, marking an index as default will disable the PyPI default index. + #[serde(skip)] + #[cfg_attr(feature = "schemars", schemars(skip))] + pub index: Option>, /// The URL of the Python package index (by default: ). /// /// Accepts either a repository compliant with [PEP 503](https://peps.python.org/pep-0503/) @@ -767,8 +837,8 @@ pub struct PipOptions { /// /// 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. #[option( default = "\"first-index\"", value_type = "str", @@ -1310,6 +1380,7 @@ pub struct PipOptions { impl From for ResolverOptions { fn from(value: ResolverInstallerOptions) -> Self { Self { + index: value.index, index_url: value.index_url, extra_index_url: value.extra_index_url, no_index: value.no_index, @@ -1339,6 +1410,7 @@ impl From for ResolverOptions { impl From for InstallerOptions { fn from(value: ResolverInstallerOptions) -> Self { Self { + index: value.index, index_url: value.index_url, extra_index_url: value.extra_index_url, no_index: value.no_index, @@ -1372,6 +1444,7 @@ impl From for InstallerOptions { #[serde(deny_unknown_fields, rename_all = "kebab-case")] #[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))] pub struct ToolOptions { + pub index: Option>, pub index_url: Option, pub extra_index_url: Option>, pub no_index: Option, @@ -1398,6 +1471,7 @@ pub struct ToolOptions { impl From for ToolOptions { fn from(value: ResolverInstallerOptions) -> Self { Self { + index: value.index, index_url: value.index_url, extra_index_url: value.extra_index_url, no_index: value.no_index, @@ -1426,6 +1500,7 @@ impl From for ToolOptions { impl From for ResolverInstallerOptions { fn from(value: ToolOptions) -> Self { Self { + index: value.index, index_url: value.index_url, extra_index_url: value.extra_index_url, no_index: value.no_index, @@ -1475,6 +1550,7 @@ pub struct OptionsWire { // #[serde(flatten)] // top_level: ResolverInstallerOptions, + index: Option>, index_url: Option, extra_index_url: Option>, no_index: Option, @@ -1539,6 +1615,7 @@ impl From for Options { concurrent_downloads, concurrent_builds, concurrent_installs, + index, index_url, extra_index_url, no_index, @@ -1592,6 +1669,7 @@ impl From for Options { concurrent_installs, }, top_level: ResolverInstallerOptions { + index, index_url, extra_index_url, no_index, diff --git a/crates/uv-workspace/Cargo.toml b/crates/uv-workspace/Cargo.toml index d77df0f98e61..2bb36ffc2d99 100644 --- a/crates/uv-workspace/Cargo.toml +++ b/crates/uv-workspace/Cargo.toml @@ -13,6 +13,7 @@ license = { workspace = true } workspace = true [dependencies] +distribution-types = { workspace = true } pep440_rs = { workspace = true } pep508_rs = { workspace = true } pypi-types = { workspace = true } @@ -20,8 +21,8 @@ uv-fs = { workspace = true, features = ["tokio", "schemars"] } uv-git = { workspace = true } uv-macros = { workspace = true } uv-normalize = { workspace = true } -uv-warnings = { workspace = true } uv-options-metadata = { workspace = true } +uv-warnings = { workspace = true } either = { workspace = true } fs-err = { workspace = true } diff --git a/crates/uv-workspace/src/pyproject.rs b/crates/uv-workspace/src/pyproject.rs index 4a212e21b095..724f50dc668c 100644 --- a/crates/uv-workspace/src/pyproject.rs +++ b/crates/uv-workspace/src/pyproject.rs @@ -6,7 +6,10 @@ //! //! Then lowers them into a dependency specification. +use distribution_types::Index; use glob::Pattern; +use pep440_rs::{Version, VersionSpecifiers}; +use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; use serde::{de::IntoDeserializer, Deserialize, Serialize}; use std::ops::Deref; use std::path::{Path, PathBuf}; @@ -15,8 +18,6 @@ use std::{collections::BTreeMap, mem}; use thiserror::Error; use url::Url; -use pep440_rs::{Version, VersionSpecifiers}; -use pypi_types::{RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; use uv_fs::{relative_to, PortablePathBuf}; use uv_git::GitReference; use uv_macros::OptionsMetadata; @@ -152,9 +153,48 @@ pub struct ToolUv { /// The sources to use (e.g., workspace members, Git repositories, local paths) when resolving /// dependencies. pub sources: Option, + + /// 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). + /// + /// 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 front of the list of + /// the list of indexes, such that it is given the highest priority when resolving packages. + /// Additionally, marking an index as default will disable the PyPI default index. + #[option( + default = "\"[]\"", + value_type = "dict", + example = r#" + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "# + )] + pub index: Option>, + /// The workspace definition for the project, if any. #[option_group] pub workspace: Option, + /// Whether the project is managed by uv. If `false`, uv will ignore the project when /// `uv run` is invoked. #[option( @@ -165,6 +205,7 @@ pub struct ToolUv { "# )] pub managed: Option, + /// Whether the project should be considered a Python package, or a non-package ("virtual") /// project. /// @@ -183,6 +224,7 @@ pub struct ToolUv { "# )] pub package: Option, + /// The project's development dependencies. Development dependencies will be installed by /// default in `uv run` and `uv sync`, but will not appear in the project's published metadata. #[cfg_attr( @@ -200,6 +242,7 @@ pub struct ToolUv { "# )] pub dev_dependencies: Option>>, + /// A list of supported environments against which to resolve dependencies. /// /// By default, uv will resolve for all possible environments during a `uv lock` operation. @@ -224,6 +267,7 @@ pub struct ToolUv { "# )] pub environments: Option, + /// Overrides to apply when resolving the project's dependencies. /// /// Overrides are used to force selection of a specific version of a package, regardless of the @@ -259,6 +303,7 @@ pub struct ToolUv { "# )] pub override_dependencies: Option>>, + /// Constraints to apply when resolving the project's dependencies. /// /// Constraints are used to restrict the versions of dependencies that are selected during @@ -450,10 +495,7 @@ pub enum Source { editable: Option, }, /// A dependency pinned to a specific index, e.g., `torch` after setting `torch` to `https://download.pytorch.org/whl/cu118`. - Registry { - // TODO(konstin): The string is more-or-less a placeholder - index: String, - }, + Registry { index: String }, /// A dependency on another package in the workspace. Workspace { /// When set to `false`, the package will be fetched from the remote index, rather than diff --git a/crates/uv-workspace/src/workspace.rs b/crates/uv-workspace/src/workspace.rs index 19fbc9baca90..0e9e42db1355 100644 --- a/crates/uv-workspace/src/workspace.rs +++ b/crates/uv-workspace/src/workspace.rs @@ -1,14 +1,14 @@ //! Resolve the current [`ProjectWorkspace`] or [`Workspace`]. +use distribution_types::Index; use either::Either; use glob::{glob, GlobError, PatternError}; +use pep508_rs::{MarkerTree, RequirementOrigin, VerbatimUrl}; +use pypi_types::{Requirement, RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; use rustc_hash::FxHashSet; use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use tracing::{debug, trace, warn}; - -use pep508_rs::{MarkerTree, RequirementOrigin, VerbatimUrl}; -use pypi_types::{Requirement, RequirementSource, SupportedEnvironments, VerbatimParsedUrl}; use uv_fs::{Simplified, CWD}; use uv_normalize::{GroupName, PackageName, DEV_DEPENDENCIES}; use uv_warnings::{warn_user, warn_user_once}; @@ -79,6 +79,10 @@ pub struct Workspace { /// /// This table is overridden by the project sources. sources: BTreeMap, + /// The index table from the workspace `pyproject.toml`. + /// + /// This table is overridden by the project indexes. + indexes: Vec, /// The `pyproject.toml` of the workspace root. pyproject_toml: PyProjectToml, } @@ -521,20 +525,9 @@ impl Workspace { &self.sources } - /// Returns an iterator over all sources in the workspace. - pub fn iter_sources(&self) -> impl Iterator { - self.packages - .values() - .filter_map(|member| { - member.pyproject_toml().tool.as_ref().and_then(|tool| { - tool.uv - .as_ref() - .and_then(|uv| uv.sources.as_ref()) - .map(ToolUvSources::inner) - .map(|sources| sources.values()) - }) - }) - .flatten() + /// The index table from the workspace `pyproject.toml`. + pub fn indexes(&self) -> &[Index] { + &self.indexes } /// The `pyproject.toml` of the workspace. @@ -751,11 +744,18 @@ impl Workspace { .and_then(|uv| uv.sources) .map(ToolUvSources::into_inner) .unwrap_or_default(); + let workspace_indexes = workspace_pyproject_toml + .tool + .clone() + .and_then(|tool| tool.uv) + .and_then(|uv| uv.index) + .unwrap_or_default(); Ok(Workspace { install_path: workspace_root, packages: workspace_members, sources: workspace_sources, + indexes: workspace_indexes, pyproject_toml: workspace_pyproject_toml, }) } @@ -1057,6 +1057,7 @@ impl ProjectWorkspace { // There may be package sources, but we don't need to duplicate them into the // workspace sources. sources: BTreeMap::default(), + indexes: Vec::default(), pyproject_toml: project_pyproject_toml.clone(), }, }); @@ -1626,6 +1627,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "bird-feeder", @@ -1677,6 +1679,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "bird-feeder", @@ -1759,6 +1762,7 @@ mod tests { "workspace": true } }, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -1777,6 +1781,7 @@ mod tests { "workspace": true } }, + "index": null, "workspace": { "members": [ "packages/*" @@ -1859,11 +1864,13 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": null, "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/*" @@ -1917,6 +1924,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2048,6 +2056,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2061,6 +2070,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/*" @@ -2146,6 +2156,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2159,6 +2170,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -2258,6 +2270,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2271,6 +2284,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", @@ -2344,6 +2358,7 @@ mod tests { } }, "sources": {}, + "indexes": [], "pyproject_toml": { "project": { "name": "albatross", @@ -2357,6 +2372,7 @@ mod tests { "tool": { "uv": { "sources": null, + "index": null, "workspace": { "members": [ "packages/seeds", diff --git a/crates/uv/src/commands/build.rs b/crates/uv/src/commands/build.rs index 6bb42f3b2fe5..397689d61315 100644 --- a/crates/uv/src/commands/build.rs +++ b/crates/uv/src/commands/build.rs @@ -391,7 +391,7 @@ async fn build_package( .into_interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/pip/compile.rs b/crates/uv/src/commands/pip/compile.rs index 60abcaefa666..995b254d1265 100644 --- a/crates/uv/src/commands/pip/compile.rs +++ b/crates/uv/src/commands/pip/compile.rs @@ -8,7 +8,7 @@ use owo_colors::OwoColorize; use tracing::debug; use distribution_types::{ - DependencyMetadata, IndexCapabilities, IndexLocations, NameRequirementSpecification, + DependencyMetadata, Index, IndexCapabilities, IndexLocations, NameRequirementSpecification, UnresolvedRequirementSpecification, Verbatim, }; use install_wheel_rs::linker::LinkMode; @@ -274,11 +274,18 @@ pub(crate) async fn pip_compile( let dev = Vec::default(); // Incorporate any index locations from the provided sources. - let index_locations = - index_locations.combine(index_url, extra_index_urls, find_links, no_index); + let index_locations = index_locations.combine( + extra_index_urls + .into_iter() + .map(Index::from_extra_index_url) + .chain(index_url.map(Index::from_index_url)) + .collect(), + find_links, + no_index, + ); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -440,12 +447,12 @@ pub(crate) async fn pip_compile( // If necessary, include the `--index-url` and `--extra-index-url` locations. if include_index_url { - if let Some(index) = index_locations.index() { - writeln!(writer, "--index-url {}", index.verbatim())?; + if let Some(index) = index_locations.default_index() { + writeln!(writer, "--index-url {}", index.url().verbatim())?; wrote_preamble = true; } - for extra_index in index_locations.extra_index() { - writeln!(writer, "--extra-index-url {}", extra_index.verbatim())?; + for extra_index in index_locations.implicit_indexes() { + writeln!(writer, "--extra-index-url {}", extra_index.url().verbatim())?; wrote_preamble = true; } } diff --git a/crates/uv/src/commands/pip/install.rs b/crates/uv/src/commands/pip/install.rs index 5e2954b6ff7d..b09741455d18 100644 --- a/crates/uv/src/commands/pip/install.rs +++ b/crates/uv/src/commands/pip/install.rs @@ -6,7 +6,7 @@ use owo_colors::OwoColorize; use tracing::{debug, enabled, Level}; use distribution_types::{ - DependencyMetadata, IndexLocations, NameRequirementSpecification, Resolution, + DependencyMetadata, Index, IndexLocations, NameRequirementSpecification, Resolution, UnresolvedRequirementSpecification, }; use install_wheel_rs::linker::LinkMode; @@ -272,11 +272,18 @@ pub(crate) async fn pip_install( let dev = Vec::default(); // Incorporate any index locations from the provided sources. - let index_locations = - index_locations.combine(index_url, extra_index_urls, find_links, no_index); + let index_locations = index_locations.combine( + extra_index_urls + .into_iter() + .map(Index::from_extra_index_url) + .chain(index_url.map(Index::from_index_url)) + .collect(), + find_links, + no_index, + ); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/pip/sync.rs b/crates/uv/src/commands/pip/sync.rs index 0721e002df00..562131cba6cd 100644 --- a/crates/uv/src/commands/pip/sync.rs +++ b/crates/uv/src/commands/pip/sync.rs @@ -5,7 +5,7 @@ use anyhow::Result; use owo_colors::OwoColorize; use tracing::debug; -use distribution_types::{DependencyMetadata, IndexLocations, Resolution}; +use distribution_types::{DependencyMetadata, Index, IndexLocations, Resolution}; use install_wheel_rs::linker::LinkMode; use pep508_rs::PackageName; use uv_auth::store_credentials_from_url; @@ -215,11 +215,18 @@ pub(crate) async fn pip_sync( }; // Incorporate any index locations from the provided sources. - let index_locations = - index_locations.combine(index_url, extra_index_urls, find_links, no_index); + let index_locations = index_locations.combine( + extra_index_urls + .into_iter() + .map(Index::from_extra_index_url) + .chain(index_url.map(Index::from_index_url)) + .collect(), + find_links, + no_index, + ); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index 1727765a2205..5db1818628d8 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -242,7 +242,7 @@ pub(crate) async fn add( resolution_environment(python_version, python_platform, target.interpreter())?; // Add all authenticated sources to the cache. - for url in settings.index_locations.urls() { + for url in settings.index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index 40819ac4b3a0..f26dd470f703 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -354,7 +354,7 @@ async fn do_lock( PythonRequirement::from_requires_python(interpreter, requires_python.clone()); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index e1aad07593f0..53d2dfef16f4 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -5,7 +5,9 @@ use itertools::Itertools; use owo_colors::OwoColorize; use tracing::debug; -use distribution_types::{Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification}; +use distribution_types::{ + Index, Resolution, UnresolvedRequirement, UnresolvedRequirementSpecification, +}; use pep440_rs::{Version, VersionSpecifiers}; use pep508_rs::MarkerTreeContents; use pypi_types::Requirement; @@ -624,7 +626,7 @@ pub(crate) async fn resolve_names( } = settings; // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -772,7 +774,7 @@ pub(crate) async fn resolve_environment<'a>( let python_requirement = PythonRequirement::from_interpreter(interpreter); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -931,7 +933,7 @@ pub(crate) async fn sync_environment( let markers = interpreter.resolver_markers(); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -1120,7 +1122,7 @@ pub(crate) async fn update_environment( } // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -1330,7 +1332,7 @@ fn warn_on_requirements_txt_setting( warn_user_once!("Ignoring `--no-index` from requirements file. Instead, use the `--no-index` command-line argument, or set `no-index` in a `uv.toml` or `pyproject.toml` file."); } else { if let Some(index_url) = index_url { - if settings.index_locations.index() != Some(index_url) { + if settings.index_locations.default_index().map(Index::url) != Some(index_url) { warn_user_once!( "Ignoring `--index-url` from requirements file: `{index_url}`. Instead, use the `--index-url` command-line argument, or set `index-url` in a `uv.toml` or `pyproject.toml` file." ); @@ -1339,8 +1341,8 @@ fn warn_on_requirements_txt_setting( for extra_index_url in extra_index_urls { if !settings .index_locations - .extra_index() - .contains(extra_index_url) + .implicit_indexes() + .any(|index| index.url() == extra_index_url) { warn_user_once!( "Ignoring `--extra-index-url` from requirements file: `{extra_index_url}`. Instead, use the `--extra-index-url` command-line argument, or set `extra-index-url` in a `uv.toml` or `pyproject.toml` file.`" diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index b854f6c40faa..28ffd05951fd 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -179,7 +179,20 @@ pub(crate) async fn run( // Install the script requirements, if necessary. Otherwise, use an isolated environment. if let Some(dependencies) = script.metadata.dependencies { - // // Collect any `tool.uv.sources` from the script. + // Collect any `tool.uv.index` from the script. + let empty = Vec::default(); + let script_indexes = match settings.sources { + SourceStrategy::Enabled => script + .metadata + .tool + .as_ref() + .and_then(|tool| tool.uv.as_ref()) + .and_then(|uv| uv.indexes.as_deref()) + .unwrap_or(&empty), + SourceStrategy::Disabled => &empty, + }; + + // Collect any `tool.uv.sources` from the script. let empty = BTreeMap::default(); let script_sources = match settings.sources { SourceStrategy::Enabled => script @@ -201,6 +214,7 @@ pub(crate) async fn run( requirement, script_dir, script_sources, + script_indexes, ) .map(LoweredRequirement::into_inner) }) diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index d6a38516f0a4..406574d12c84 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -254,7 +254,7 @@ pub(super) async fn do_sync( let resolution = apply_editable_mode(resolution, editable); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { uv_auth::store_credentials_from_url(url); } diff --git a/crates/uv/src/commands/venv.rs b/crates/uv/src/commands/venv.rs index 46c04c5459e6..af963e47c430 100644 --- a/crates/uv/src/commands/venv.rs +++ b/crates/uv/src/commands/venv.rs @@ -226,7 +226,7 @@ async fn venv_impl( let interpreter = python.into_interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } @@ -275,7 +275,7 @@ async fn venv_impl( let interpreter = venv.interpreter(); // Add all authenticated sources to the cache. - for url in index_locations.urls() { + for url in index_locations.allowed_urls() { store_credentials_from_url(url); } diff --git a/crates/uv/src/settings.rs b/crates/uv/src/settings.rs index b4bb325c3333..49e649e72268 100644 --- a/crates/uv/src/settings.rs +++ b/crates/uv/src/settings.rs @@ -4,7 +4,7 @@ use std::path::PathBuf; use std::process; use std::str::FromStr; -use distribution_types::{DependencyMetadata, IndexLocations}; +use distribution_types::{DependencyMetadata, Index, IndexLocations}; use install_wheel_rs::linker::LinkMode; use pep508_rs::{ExtraName, RequirementOrigin}; use pypi_types::{Requirement, SupportedEnvironments}; @@ -1900,8 +1900,19 @@ impl From for ResolverSettings { fn from(value: ResolverOptions) -> Self { Self { index_locations: IndexLocations::new( - value.index_url, - value.extra_index_url.unwrap_or_default(), + value + .index + .into_iter() + .flatten() + .chain( + value + .extra_index_url + .into_iter() + .flatten() + .map(Index::from_extra_index_url), + ) + .chain(value.index_url.into_iter().map(Index::from_index_url)) + .collect(), value.find_links.unwrap_or_default(), value.no_index.unwrap_or_default(), ), @@ -2027,8 +2038,19 @@ impl From for ResolverInstallerSettings { fn from(value: ResolverInstallerOptions) -> Self { Self { index_locations: IndexLocations::new( - value.index_url, - value.extra_index_url.unwrap_or_default(), + value + .index + .into_iter() + .flatten() + .chain( + value + .extra_index_url + .into_iter() + .flatten() + .map(Index::from_extra_index_url), + ) + .chain(value.index_url.into_iter().map(Index::from_index_url)) + .collect(), value.find_links.unwrap_or_default(), value.no_index.unwrap_or_default(), ), @@ -2134,6 +2156,7 @@ impl PipSettings { break_system_packages, target, prefix, + index, index_url, extra_index_url, no_index, @@ -2185,6 +2208,7 @@ impl PipSettings { } = pip.unwrap_or_default(); let ResolverInstallerOptions { + index: top_level_index, index_url: top_level_index_url, extra_index_url: top_level_extra_index_url, no_index: top_level_no_index, @@ -2216,6 +2240,7 @@ impl PipSettings { // preferring the latter. // // For example, prefer `tool.uv.pip.index-url` over `tool.uv.index-url`. + let index = index.combine(top_level_index); let index_url = index_url.combine(top_level_index_url); let extra_index_url = extra_index_url.combine(top_level_extra_index_url); let no_index = no_index.combine(top_level_no_index); @@ -2241,10 +2266,25 @@ impl PipSettings { Self { index_locations: IndexLocations::new( - args.index_url.combine(index_url), - args.extra_index_url - .combine(extra_index_url) - .unwrap_or_default(), + args.index + .into_iter() + .flatten() + .chain( + args.extra_index_url + .into_iter() + .flatten() + .map(Index::from_extra_index_url), + ) + .chain(args.index_url.into_iter().map(Index::from_index_url)) + .chain(index.into_iter().flatten()) + .chain( + extra_index_url + .into_iter() + .flatten() + .map(Index::from_extra_index_url), + ) + .chain(index_url.into_iter().map(Index::from_index_url)) + .collect(), args.find_links.combine(find_links).unwrap_or_default(), args.no_index.combine(no_index).unwrap_or_default(), ), diff --git a/crates/uv/tests/lock.rs b/crates/uv/tests/lock.rs index 4343628287e5..50a17366d507 100644 --- a/crates/uv/tests/lock.rs +++ b/crates/uv/tests/lock.rs @@ -7537,7 +7537,7 @@ fn lock_local_index() -> Result<()> { [tool.uv] extra-index-url = ["{}"] "#, - Url::from_directory_path(&root).unwrap().as_str() + Url::from_file_path(&root).unwrap().as_str() })?; uv_snapshot!(context.filters(), context.lock().env_remove("UV_EXCLUDE_NEWER"), @r###" @@ -7551,7 +7551,7 @@ fn lock_local_index() -> Result<()> { let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); - let index = Url::from_directory_path(&root).unwrap().to_string(); + let index = Url::from_file_path(&root).unwrap().to_string(); let filters = [(index.as_str(), "file://[TMP]")] .into_iter() .chain(context.filters()) @@ -11505,6 +11505,443 @@ fn lock_trailing_slash() -> Result<()> { Ok(()) } +#[test] +fn lock_explicit_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0", "iniconfig==2.0.0"] + + [build-system] + requires = ["setuptools>=42"] + build-backend = "setuptools.build_meta" + + [tool.uv.sources] + iniconfig = { index = "test" } + + [[tool.uv.index]] + name = "test" + url = "https://test.pypi.org/simple" + explicit = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 5 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "anyio" + version = "3.7.0" + source = { registry = "https://pypi.org/simple" } + dependencies = [ + { name = "idna" }, + { name = "sniffio" }, + ] + sdist = { url = "https://files.pythonhosted.org/packages/c6/b3/fefbf7e78ab3b805dec67d698dc18dd505af7a18a8dd08868c9b4fa736b5/anyio-3.7.0.tar.gz", hash = "sha256:275d9973793619a5374e1c89a4f4ad3f4b0a5510a2b5b939444bee8f4c4d37ce", size = 142737 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/68/fe/7ce1926952c8a403b35029e194555558514b365ad77d75125f521a2bec62/anyio-3.7.0-py3-none-any.whl", hash = "sha256:eddca883c4175f14df8aedce21054bfca3adb70ffe76a9f607aef9d7fa2ea7f0", size = 80873 }, + ] + + [[package]] + name = "idna" + version = "3.6" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/bf/3f/ea4b9117521a1e9c50344b909be7886dd00a519552724809bb1f486986c2/idna-3.6.tar.gz", hash = "sha256:9ecdbbd083b06798ae1e86adcbfe8ab1479cf864e4ee30fe4e46a003d12491ca", size = 175426 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/e7/a82b05cf63a603df6e68d59ae6a68bf5064484a0718ea5033660af4b54a9/idna-3.6-py3-none-any.whl", hash = "sha256:c05567e9c24a6b9faaa835c4821bad0590fbb9d5779e7caa6e1cc4978e7eb24f", size = 61567 }, + ] + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://test.pypi.org/simple" } + sdist = { url = "https://test-files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://test-files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { editable = "." } + dependencies = [ + { name = "anyio" }, + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [ + { name = "anyio", specifier = "==3.7.0" }, + { name = "iniconfig", specifier = "==2.0.0", index = "https://test.pypi.org/simple" }, + ] + + [[package]] + name = "sniffio" + version = "1.3.1" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/a2/87/a6771e1546d97e7e041b6ae58d80074f81b7d5121207425c964ddf5cfdbd/sniffio-1.3.1.tar.gz", hash = "sha256:f4324edc670a0f49750a81b895f35c3adb843cca46f0530f79fc1babb23789dc", size = 20372 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235 }, + ] + "### + ); + }); + + Ok(()) +} + +#[test] +fn lock_named_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["typing-extensions"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + explicit = true + + [[tool.uv.index]] + name = "heron" + url = "https://pypi-proxy.fly.dev/simple" + + [[tool.uv.index]] + name = "test" + url = "https://test.pypi.org/simple" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "typing-extensions" }, + ] + + [package.metadata] + requires-dist = [{ name = "typing-extensions" }] + + [[package]] + name = "typing-extensions" + version = "4.10.0" + source = { registry = "https://pypi-proxy.fly.dev/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/16/3a/0d26ce356c7465a19c9ea8814b960f8a36c3b0d07c323176620b7b483e44/typing_extensions-4.10.0.tar.gz", hash = "sha256:b0abd7c89e8fb96f98db18d86106ff1d90ab692004eb746cf6eda2682f91b3cb", size = 77558 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/f9/de/dc04a3ea60b22624b51c703a84bbe0184abcd1d0b9bc8074b5d6b7ab90bb/typing_extensions-4.10.0-py3-none-any.whl", hash = "sha256:69b1a937c3a517342112fb4c6df7e72fc39a38e7891a5730ed4985b5214b5475", size = 33926 }, + ] + "### + ); + }); + + Ok(()) +} + +#[test] +fn lock_default_index() -> Result<()> { + let context = TestContext::new("3.12"); + + // If an index is included, PyPI will still be used as the default index. + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); + + // Unless that index is marked as the default. + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + default = true + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 1 + ----- stdout ----- + + ----- stderr ----- + × No solution found when resolving dependencies: + ╰─▶ Because iniconfig was not found in the package registry and your project depends on iniconfig, we can conclude that your project's requirements are unsatisfiable. + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); + + Ok(()) +} + +#[test] +fn lock_explicit_index_cli() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["anyio==3.7.0", "jinja2"] + + [tool.uv.sources] + jinja2 = { index = "pytorch" } + "#, + )?; + + // The package references a non-existent index. + uv_snapshot!(context.filters(), context.lock(), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Failed to parse entry for: `jinja2` + Caused by: Package `jinja2` references an undeclared index: `pytorch` + "###); + + // This also isn't supported right now; you need to specify the index in the `pyproject.toml`. + uv_snapshot!(context.filters(), context.lock().arg("--index").arg("pytorch=https://download.pytorch.org/whl/cu121"), @r###" + success: false + exit_code: 2 + ----- stdout ----- + + ----- stderr ----- + error: Failed to build: `project @ file://[TEMP_DIR]/` + Caused by: Failed to parse entry for: `jinja2` + Caused by: Package `jinja2` references an undeclared index: `pytorch` + "###); + + Ok(()) +} + +/// If a name is reused, the higher-priority index should "overwrite" the lower-priority index. +/// In other words, the lower-priority index should be ignored entirely during implicit resolution. +/// +/// In this test, we should use PyPI (the default index) rather than falling back to Test PyPI, +/// which should be ignored. +#[test] +fn lock_repeat_named_index() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "project" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = ["iniconfig"] + + [[tool.uv.index]] + name = "pytorch" + url = "https://download.pytorch.org/whl/cu121" + + [[tool.uv.index]] + name = "pytorch" + url = "https://test.pypi.org/simple" + "#, + )?; + + uv_snapshot!(context.filters(), context.lock(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + "###); + + let lock = fs_err::read_to_string(context.temp_dir.join("uv.lock")).unwrap(); + + insta::with_settings!({ + filters => context.filters(), + }, { + assert_snapshot!( + lock, @r###" + version = 1 + requires-python = ">=3.12" + + [options] + exclude-newer = "2024-03-25T00:00:00Z" + + [[package]] + name = "iniconfig" + version = "2.0.0" + source = { registry = "https://pypi.org/simple" } + sdist = { url = "https://files.pythonhosted.org/packages/d7/4b/cbd8e699e64a6f16ca3a8220661b5f83792b3017d0f79807cb8708d33913/iniconfig-2.0.0.tar.gz", hash = "sha256:2d91e135bf72d31a410b17c16da610a82cb55f6b0477d1a902134b24a455b8b3", size = 4646 } + wheels = [ + { url = "https://files.pythonhosted.org/packages/ef/a6/62565a6e1cf69e10f5727360368e451d4b7f58beeac6173dc9db836a5b46/iniconfig-2.0.0-py3-none-any.whl", hash = "sha256:b6a85871a79d2e3b22d2d1b94ac2824226a63c6b741c88f7ae975f18b6778374", size = 5892 }, + ] + + [[package]] + name = "project" + version = "0.1.0" + source = { virtual = "." } + dependencies = [ + { name = "iniconfig" }, + ] + + [package.metadata] + requires-dist = [{ name = "iniconfig" }] + "### + ); + }); + + Ok(()) +} + /// Lock a project with `package = false`, making it a virtual project. #[test] fn lock_explicit_virtual_project() -> Result<()> { diff --git a/crates/uv/tests/pip_install.rs b/crates/uv/tests/pip_install.rs index 1c1205c5b83f..d1fd79917b19 100644 --- a/crates/uv/tests/pip_install.rs +++ b/crates/uv/tests/pip_install.rs @@ -194,7 +194,7 @@ fn invalid_pyproject_toml_option_unknown_field() -> Result<()> { | 2 | unknown = "field" | ^^^^^^^ - unknown field `unknown`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` + unknown field `unknown`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` Resolved in [TIME] Audited in [TIME] diff --git a/crates/uv/tests/show_settings.rs b/crates/uv/tests/show_settings.rs index dcad6fac7834..0bded9244d37 100644 --- a/crates/uv/tests/show_settings.rs +++ b/crates/uv/tests/show_settings.rs @@ -92,31 +92,35 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -234,31 +238,35 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -377,31 +385,35 @@ fn resolve_uv_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -552,31 +564,35 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -696,8 +712,7 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -826,31 +841,35 @@ fn resolve_pyproject_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -993,53 +1012,61 @@ fn resolve_index_url() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), - ), - extra_index: [ - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "test.pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), + ), + explicit: false, + default: true, + }, ], flat_index: [], no_index: false, @@ -1160,75 +1187,88 @@ fn resolve_index_url() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "test.pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), - ), - extra_index: [ - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "test.pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), + ), + explicit: false, + default: true, + }, ], flat_index: [], no_index: false, @@ -1372,8 +1412,7 @@ fn resolve_find_links() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [ Url( VerbatimUrl { @@ -1538,8 +1577,7 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -1674,52 +1712,61 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [ - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), ), + port: None, + path: "/whl", + query: None, + fragment: None, + }, + given: Some( + "https://download.pytorch.org/whl", ), - port: None, - path: "/whl", - query: None, - fragment: None, }, - given: Some( - "https://download.pytorch.org/whl", - ), - }, - ), - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "test.pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), + ), + explicit: false, + default: false, + }, ], flat_index: [], no_index: false, @@ -1838,52 +1885,61 @@ fn resolve_top_level() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [ - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "download.pytorch.org", + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "download.pytorch.org", + ), ), + port: None, + path: "/whl", + query: None, + fragment: None, + }, + given: Some( + "https://download.pytorch.org/whl", ), - port: None, - path: "/whl", - query: None, - fragment: None, }, - given: Some( - "https://download.pytorch.org/whl", - ), - }, - ), - Url( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "test.pypi.org", + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "test.pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://test.pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://test.pypi.org/simple", - ), - }, - ), + ), + explicit: false, + default: false, + }, ], flat_index: [], no_index: false, @@ -2026,8 +2082,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2152,8 +2207,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2278,8 +2332,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2406,8 +2459,7 @@ fn resolve_user_configuration() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2546,6 +2598,7 @@ fn resolve_tool() -> anyhow::Result<()> { ), ), options: ResolverInstallerOptions { + index: None, index_url: None, extra_index_url: None, no_index: None, @@ -2582,8 +2635,7 @@ fn resolve_tool() -> anyhow::Result<()> { }, settings: ResolverInstallerSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2710,8 +2762,7 @@ fn resolve_poetry_toml() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -2864,31 +2915,35 @@ fn resolve_both() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -3033,31 +3088,35 @@ fn resolve_config_file() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: Some( - Pypi( - VerbatimUrl { - url: Url { - scheme: "https", - cannot_be_a_base: false, - username: "", - password: None, - host: Some( - Domain( - "pypi.org", + indexes: [ + Index { + name: None, + url: Pypi( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "pypi.org", + ), ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://pypi.org/simple", ), - port: None, - path: "/simple", - query: None, - fragment: None, }, - given: Some( - "https://pypi.org/simple", - ), - }, - ), - ), - extra_index: [], + ), + explicit: false, + default: true, + }, + ], flat_index: [], no_index: false, }, @@ -3150,7 +3209,7 @@ fn resolve_config_file() -> anyhow::Result<()> { | 1 | [project] | ^^^^^^^ - unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` + unknown field `project`, expected one of `native-tls`, `offline`, `no-cache`, `cache-dir`, `preview`, `python-preference`, `python-downloads`, `concurrent-downloads`, `concurrent-builds`, `concurrent-installs`, `index`, `index-url`, `extra-index-url`, `no-index`, `find-links`, `index-strategy`, `keyring-provider`, `allow-insecure-host`, `resolution`, `prerelease`, `dependency-metadata`, `config-settings`, `no-build-isolation`, `no-build-isolation-package`, `exclude-newer`, `link-mode`, `compile-bytecode`, `no-sources`, `upgrade`, `upgrade-package`, `reinstall`, `reinstall-package`, `no-build`, `no-build-package`, `no-binary`, `no-binary-package`, `publish-url`, `trusted-publishing`, `pip`, `cache-keys`, `override-dependencies`, `constraint-dependencies`, `environments`, `workspace`, `sources`, `dev-dependencies`, `managed`, `package` "### ); @@ -3277,8 +3336,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -3406,8 +3464,7 @@ fn resolve_skip_empty() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -3543,8 +3600,7 @@ fn allow_insecure_host() -> anyhow::Result<()> { ), settings: PipSettings { index_locations: IndexLocations { - index: None, - extra_index: [], + indexes: [], flat_index: [], no_index: false, }, @@ -3622,3 +3678,1078 @@ fn allow_insecure_host() -> anyhow::Result<()> { Ok(()) } + +/// Deserialize an insecure host. +#[test] +#[cfg_attr( + windows, + ignore = "Configuration tests are not yet supported on Windows" +)] +fn index_priority() -> anyhow::Result<()> { + let context = TestContext::new("3.12"); + + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [[index]] + url = "https://file.pypi.org/simple" + "#})?; + + let requirements_in = context.temp_dir.child("requirements.in"); + requirements_in.write_str("anyio>3.0.0")?; + + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--index-url") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: OnlySystem, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: false, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--default-index") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: OnlySystem, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: false, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + index-url = "https://file.pypi.org/simple" + "#})?; + + // Prefer the `--default-index` from the CLI, and treat it as the default. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--default-index") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: OnlySystem, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + // Prefer the `--index` from the CLI, but treat the index from the file as the default. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--index") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: OnlySystem, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + let config = context.temp_dir.child("uv.toml"); + config.write_str(indoc::indoc! {r#" + [[index]] + url = "https://file.pypi.org/simple" + default = true + "#})?; + + // Prefer the `--index-url` from the CLI, and treat it as the default. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--index-url") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: OnlySystem, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + // Prefer the `--extra-index-url` from the CLI, but not as the default. + uv_snapshot!(context.filters(), add_shared_args(context.pip_compile()) + .arg("requirements.in") + .arg("--show-settings") + .arg("--extra-index-url") + .arg("https://cli.pypi.org/simple"), @r###" + success: true + exit_code: 0 + ----- stdout ----- + GlobalSettings { + quiet: false, + verbose: 0, + color: Auto, + native_tls: false, + concurrency: Concurrency { + downloads: 50, + builds: 16, + installs: 8, + }, + connectivity: Online, + show_settings: true, + preview: Disabled, + python_preference: OnlySystem, + python_downloads: Automatic, + no_progress: false, + } + CacheSettings { + no_cache: false, + cache_dir: Some( + "[CACHE_DIR]/", + ), + } + PipCompileSettings { + src_file: [ + "requirements.in", + ], + constraint: [], + override: [], + build_constraint: [], + constraints_from_workspace: [], + overrides_from_workspace: [], + environments: SupportedEnvironments( + [], + ), + refresh: None( + Timestamp( + SystemTime { + tv_sec: [TIME], + tv_nsec: [TIME], + }, + ), + ), + settings: PipSettings { + index_locations: IndexLocations { + indexes: [ + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "cli.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://cli.pypi.org/simple", + ), + }, + ), + explicit: false, + default: false, + }, + Index { + name: None, + url: Url( + VerbatimUrl { + url: Url { + scheme: "https", + cannot_be_a_base: false, + username: "", + password: None, + host: Some( + Domain( + "file.pypi.org", + ), + ), + port: None, + path: "/simple", + query: None, + fragment: None, + }, + given: Some( + "https://file.pypi.org/simple", + ), + }, + ), + explicit: false, + default: true, + }, + ], + flat_index: [], + no_index: false, + }, + python: None, + system: false, + extras: None, + break_system_packages: false, + target: None, + prefix: None, + index_strategy: FirstIndex, + keyring_provider: Disabled, + allow_insecure_host: [], + no_build_isolation: false, + no_build_isolation_package: [], + build_options: BuildOptions { + no_binary: None, + no_build: None, + }, + allow_empty_requirements: false, + strict: false, + dependency_mode: Transitive, + resolution: Highest, + prerelease: IfNecessaryOrExplicit, + dependency_metadata: DependencyMetadata( + {}, + ), + output_file: None, + no_strip_extras: false, + no_strip_markers: false, + no_annotate: false, + no_header: false, + custom_compile_command: None, + generate_hashes: false, + config_setting: ConfigSettings( + {}, + ), + python_version: None, + python_platform: None, + universal: false, + exclude_newer: Some( + ExcludeNewer( + 2024-03-25T00:00:00Z, + ), + ), + no_emit_package: [], + emit_index_url: false, + emit_find_links: false, + emit_build_options: false, + emit_marker_expression: false, + emit_index_annotation: false, + annotation_style: Split, + link_mode: Clone, + compile_bytecode: false, + sources: Enabled, + hash_checking: None, + upgrade: None, + reinstall: None, + }, + } + + ----- stderr ----- + "### + ); + + Ok(()) +} diff --git a/crates/uv/tests/sync.rs b/crates/uv/tests/sync.rs index eb3998cead1e..bfe6a7daa82e 100644 --- a/crates/uv/tests/sync.rs +++ b/crates/uv/tests/sync.rs @@ -2803,3 +2803,60 @@ fn sync_no_sources_missing_member() -> Result<()> { Ok(()) } + +#[test] +fn sync_explicit() -> Result<()> { + let context = TestContext::new("3.12"); + + let pyproject_toml = context.temp_dir.child("pyproject.toml"); + pyproject_toml.write_str( + r#" + [project] + name = "root" + version = "0.1.0" + requires-python = ">=3.12" + dependencies = [ + "idna>2", + ] + + [[tool.uv.index]] + name = "test" + url = "https://test.pypi.org/simple" + explicit = true + + [tool.uv.sources] + idna = { index = "test" } + "#, + )?; + + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Resolved 2 packages in [TIME] + Prepared 1 package in [TIME] + Installed 1 package in [TIME] + + idna==2.7 + "###); + + // Clear the environment. + fs_err::remove_dir_all(&context.venv)?; + + // The package should be drawn from the cache. + uv_snapshot!(context.filters(), context.sync(), @r###" + success: true + exit_code: 0 + ----- stdout ----- + + ----- stderr ----- + Using CPython 3.12.[X] interpreter at: [PYTHON-3.12] + Creating virtual environment at: .venv + Resolved 2 packages in [TIME] + Installed 1 package in [TIME] + + idna==2.7 + "###); + + Ok(()) +} diff --git a/docs/concepts/dependencies.md b/docs/concepts/dependencies.md index e45ea9dcdbeb..9afd69b961fd 100644 --- a/docs/concepts/dependencies.md +++ b/docs/concepts/dependencies.md @@ -70,6 +70,7 @@ stands-compliant `project.dependencies` table. During development, a project may rely on a package that isn't available on PyPI. The following additional sources are supported by uv: +- Index: A package from an explicit package index. - Git: A Git repository. - URL: A remote wheel or source distribution. - Path: A local wheel, source distribution, or project directory. @@ -91,6 +92,29 @@ $ uv lock --no-sources The use of `--no-sources` will also prevent uv from discovering any [workspace members](#workspace-member) that could satisfy a given dependency. +### Index + +To pin a Python package to a specific index, add a named index to the `pyproject.toml`: + +```toml title="pyproject.toml" +[project] +dependencies = [ + "torch", +] + +[tool.uv.sources] +torch = { index = "pytorch" } + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +explicit = true +``` + +The `explicit` flag is optional and indicates that the index should _only_ be used for packages that +explicitly specify it in `tool.uv.sources`. If `explicit` is not set, other packages may be resolved +from the index, if not found elsewhere. + ### Git To add a Git dependency source, prefix a Git-compatible URL to clone with `git+`. diff --git a/docs/configuration/environment.md b/docs/configuration/environment.md index 917ebd1649cd..0e16a13276fb 100644 --- a/docs/configuration/environment.md +++ b/docs/configuration/environment.md @@ -2,10 +2,15 @@ uv accepts the following command-line arguments as environment variables: +- `UV_INDEX`: Equivalent to the `--index` command-line argument. If set, uv will use this URL as the + base index for searching for packages. +- `UV_DEFAULT_INDEX`: Equivalent to the `--default-index` command-line argument. If set, uv will use + this space-separated list of URLs as additional indexes when searching for packages. - `UV_INDEX_URL`: Equivalent to the `--index-url` command-line argument. If set, uv will use this - URL as the base index for searching for packages. + URL as the base index for searching for packages. (Deprecated: use `UV_INDEX` instead.) - `UV_EXTRA_INDEX_URL`: Equivalent to the `--extra-index-url` command-line argument. If set, uv will use this space-separated list of URLs as additional indexes when searching for packages. + (Deprecated: use `UV_DEFAULT_INDEX` instead.) - `UV_CACHE_DIR`: Equivalent to the `--cache-dir` command-line argument. If set, uv will use this directory for caching instead of the default cache directory. - `UV_NO_CACHE`: Equivalent to the `--no-cache` command-line argument. If set, uv will not use the diff --git a/docs/configuration/index.md b/docs/configuration/index.md index 8f0bc1f3edce..38c68875fc1e 100644 --- a/docs/configuration/index.md +++ b/docs/configuration/index.md @@ -5,6 +5,7 @@ Read about the various ways to configure uv: - [Using configuration files](./files.md) - [Using environment variables](./environment.md) - [Configuring authentication](./authentication.md) +- [Configuring package indexes](./indexes.md) Or, jump to the [settings reference](../reference/settings.md) which enumerates the available configuration options. diff --git a/docs/configuration/indexes.md b/docs/configuration/indexes.md new file mode 100644 index 000000000000..70ef968f5f9a --- /dev/null +++ b/docs/configuration/indexes.md @@ -0,0 +1,114 @@ +# Package indexes + +By default, uv uses the [Python Package Index (PyPI)](https://pypi.org) for dependency resolution +and package installation. However, uv can be configured to use other package indexes, including +private indexes, via the `[[tool.uv.index]]` configuration option (and `--index`, its analogous +command-line option). + +## Defining an index + +To include an additional index when resolving dependencies, add a `[[tool.uv.index]]` entry to your +`pyproject.toml`: + +```toml +[[tool.uv.index]] +# Optional, explicit name for the index. +name = "pytorch" +# Required URL for the index. Expects a repository compliant with PEP 503 (the simple repository API). +url = "https://download.pytorch.org/whl/cpu" +``` + +Indexes are prioritized in the order in which they’re defined, such that the first index listed in +the configuration file is the first index consulted when resolving dependencies, with indexes +provided via the command line taking precedence over those in the configuration file. + +By default, uv includes the Python Package Index (PyPI) as the "default" index, i.e., the index used +when a package is not found on any other index. To exclude PyPI from the list of indexes, set +`default = true` on another index entry (or use the `--default-index` command-line option): + +```toml +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +default = true +``` + +The default index is always treated as lowest priority, regardless of its position in the list of +indexes. + +## Pinning a package to an index + +A package can be pinned to a specific index by specifying the index in its `tool.uv.sources` entry. +For example, to ensure that `torch` is _always_ installed from the `pytorch` index, add the +following to your `pyproject.toml`: + +```toml +[tool.uv.sources] +torch = { index = "pytorch" } + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +``` + +An index can be marked as `explicit = true` to prevent packages from being installed from that index +unless explicitly pinned to it. For example, to ensure that `torch` is _only_ installed from the +`pytorch` index, add the following to your `pyproject.toml`: + +```toml +[tool.uv.sources] +torch = { index = "pytorch" } + +[[tool.uv.index]] +name = "pytorch" +url = "https://download.pytorch.org/whl/cpu" +explicit = true +``` + +Named indexes referenced via `tool.uv.sources` must be defined within the project's `pyproject.toml` +file; indexes provided via the command-line, environment variables, or user-level configuration will +not be recognized. + +## Searching across multiple indexes + +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`). + +For example, if an internal index is specified via `[[tool.uv.index]]`, uv's behavior is such that +if a package exists on that internal index, it will _always_ be installed from that internal index, +and never from PyPI. The intent is to prevent "dependency confusion" attacks, in which an attacker +publishes a malicious package on PyPI with the same name as an internal package, thus causing the +malicious package to be installed instead of the internal package. See, for example, +[the `torchtriton` attack](https://pytorch.org/blog/compromised-nightly-dependency/) from +December 2022. + +Users can opt in to alternate index behaviors via the`--index-strategy` command-line option, or the +`UV_INDEX_STRATEGY` environment variable, which supports the following values: + +- `first-match` (default): Search for each package across all indexes, limiting the candidate + versions to those present in the first index that contains the package. +- `unsafe-first-match`: Search for each package across all indexes, but prefer the first index with + a compatible version, even if newer versions are available on other indexes. +- `unsafe-best-match`: Search for each package across all indexes, and select the best version from + the combined set of candidate versions. + +While `unsafe-best-match` is the closest to pip's behavior, it exposes users to the risk of +"dependency confusion" attacks. + +## `--index-url` and `--extra-index-url` + +In addition to the `[[tool.uv.index]]` configuration option, uv supports pip-style `--index-url` and +`--extra-index-url` command-line options for compatibility, where `--index-url` defines the default +index and `--extra-index-url` defines additional indexes. + +These options can be used in conjunction with the `[[tool.uv.index]]` configuration option, and use +the same prioritization rules: + +- The default index is always treated as lowest priority, whether defined via the legacy + `--index-url` argument, the recommended `--default-index` argument, or a `[[tool.uv.index]]` entry + with `default = true`. +- Indexes are consulted in the order in which they’re defined, either via the legacy + `--extra-index-url` argument, the recommended `--index` argument, or `[[tool.uv.index]]` entries. + +In effect, `--index-url` and `--extra-index-url` can be thought of as unnamed `[[tool.uv.index]]` +entries, with `default = true` enabled for the former. diff --git a/docs/pip/compatibility.md b/docs/pip/compatibility.md index 128ff23d8956..cab59ca1769b 100644 --- a/docs/pip/compatibility.md +++ b/docs/pip/compatibility.md @@ -148,10 +148,9 @@ supports the following values: While `unsafe-best-match` is the closest to `pip`'s behavior, it exposes users to the risk of "dependency confusion" attacks. -In the future, uv will support pinning packages to dedicated indexes (see: -[#171](https://github.com/astral-sh/uv/issues/171)). Additionally, -[PEP 708](https://peps.python.org/pep-0708/) is a provisional standard that aims to address the -"dependency confusion" issue across package registries and installers. +uv also supports pinning packages to dedicated indexes (see: +[_Indexes_](../configuration/indexes.md#pinning-a-package-to-an-index)), such that a given package +is _always_ installed from a specific index. ## PEP 517 build isolation diff --git a/docs/reference/cli.md b/docs/reference/cli.md index 4b0c56f3071c..cae8a3e0245b 100644 --- a/docs/reference/cli.md +++ b/docs/reference/cli.md @@ -116,6 +116,13 @@ uv run [OPTIONS]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -141,6 +148,8 @@ uv run [OPTIONS]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -154,9 +163,16 @@ uv run [OPTIONS]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -174,6 +190,8 @@ uv run [OPTIONS]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--isolated

Run the command in an isolated virtual environment.

@@ -654,6 +672,13 @@ uv add [OPTIONS] >

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--dev

Add the requirements as development dependencies

--directory directory

Change to the given directory prior to running the command.

@@ -681,6 +706,8 @@ uv add [OPTIONS] >

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -694,9 +721,16 @@ uv add [OPTIONS] >
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -714,6 +748,8 @@ uv add [OPTIONS] >

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -979,6 +1015,13 @@ uv remove [OPTIONS] ...

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--dev

Remove the packages from the development dependencies

--directory directory

Change to the given directory prior to running the command.

@@ -998,6 +1041,8 @@ uv remove [OPTIONS] ...

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -1011,9 +1056,16 @@ uv remove [OPTIONS] ...
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -1031,6 +1083,8 @@ uv remove [OPTIONS] ...

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -1280,6 +1334,13 @@ uv sync [OPTIONS]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -1303,6 +1364,8 @@ uv sync [OPTIONS]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -1316,9 +1379,16 @@ uv sync [OPTIONS]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -1336,6 +1406,8 @@ uv sync [OPTIONS]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--inexact

Do not remove extraneous packages present in the environment.

@@ -1589,6 +1661,13 @@ uv lock [OPTIONS]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -1606,6 +1685,8 @@ uv lock [OPTIONS]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -1617,9 +1698,16 @@ uv lock [OPTIONS]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -1637,6 +1725,8 @@ uv lock [OPTIONS]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -1864,6 +1954,13 @@ uv export [OPTIONS]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -1885,6 +1982,8 @@ uv export [OPTIONS]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -1908,9 +2007,16 @@ uv export [OPTIONS]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -1928,6 +2034,8 @@ uv export [OPTIONS]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -2173,6 +2281,13 @@ uv tree [OPTIONS]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--depth, -d depth

Maximum display depth of the dependency tree

[default: 255]

@@ -2193,6 +2308,8 @@ uv tree [OPTIONS]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -2206,9 +2323,16 @@ uv tree [OPTIONS]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -2226,6 +2350,8 @@ uv tree [OPTIONS]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--invert

Show the reverse dependencies for the given package. This flag will invert the tree and display the packages that depend on the given package

@@ -2547,6 +2673,13 @@ uv tool run [OPTIONS] [COMMAND]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -2564,6 +2697,8 @@ uv tool run [OPTIONS] [COMMAND]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -2577,9 +2712,16 @@ uv tool run [OPTIONS] [COMMAND]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -2597,6 +2739,8 @@ uv tool run [OPTIONS] [COMMAND]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--isolated

Run the tool in an isolated virtual environment, ignoring any already-installed tools

@@ -2835,6 +2979,13 @@ uv tool install [OPTIONS]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -2852,6 +3003,8 @@ uv tool install [OPTIONS]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -2865,9 +3018,16 @@ uv tool install [OPTIONS]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -2885,6 +3045,8 @@ uv tool install [OPTIONS]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -3119,6 +3281,13 @@ uv tool upgrade [OPTIONS] ...

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -3136,6 +3305,8 @@ uv tool upgrade [OPTIONS] ...

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -3145,9 +3316,16 @@ uv tool upgrade [OPTIONS] ...
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -3165,6 +3343,8 @@ uv tool upgrade [OPTIONS] ...

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -4671,6 +4851,13 @@ uv pip compile [OPTIONS] ...

Used to reflect custom build scripts and commands that wrap uv pip compile.

May also be set with the UV_CUSTOM_COMPILE_COMMAND environment variable.

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -4700,6 +4887,8 @@ uv pip compile [OPTIONS] ...

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -4711,9 +4900,16 @@ uv pip compile [OPTIONS] ...
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -4731,6 +4927,8 @@ uv pip compile [OPTIONS] ...

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -5069,6 +5267,13 @@ uv pip sync [OPTIONS] ...

This is equivalent to pip’s --constraint option.

May also be set with the UV_CONSTRAINT environment variable.

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -5088,6 +5293,8 @@ uv pip sync [OPTIONS] ...

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -5097,9 +5304,16 @@ uv pip sync [OPTIONS] ...
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -5117,6 +5331,8 @@ uv pip sync [OPTIONS] ...

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -5405,6 +5621,13 @@ uv pip install [OPTIONS] |--editable This is equivalent to pip’s --constraint option.

May also be set with the UV_CONSTRAINT environment variable.

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -5430,6 +5653,8 @@ uv pip install [OPTIONS] |--editable All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -5439,9 +5664,16 @@ uv pip install [OPTIONS] |--editable
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -5459,6 +5691,8 @@ uv pip install [OPTIONS] |--editable The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -6582,6 +6816,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-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -6599,6 +6840,8 @@ uv venv [OPTIONS] [PATH]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -6608,9 +6851,16 @@ uv venv [OPTIONS] [PATH]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -6628,6 +6878,8 @@ uv venv [OPTIONS] [PATH]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

@@ -6829,6 +7081,13 @@ uv build [OPTIONS] [SRC]

May also be set with the UV_CONFIG_FILE environment variable.

--config-setting, -C config-setting

Settings to pass to the PEP 517 build backend, specified as KEY=VALUE pairs

+
--default-index default-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.

--directory directory

Change to the given directory prior to running the command.

Relative paths are resolved with the given directory as the base.

@@ -6846,6 +7105,8 @@ uv build [OPTIONS] [SRC]

All indexes provided via this flag take priority over the index specified by --index-url (which defaults to PyPI). When multiple --extra-index-url flags are provided, earlier values take priority.

+

(Deprecated: use --index instead.)

+

May also be set with the UV_EXTRA_INDEX_URL environment variable.

--find-links, -f find-links

Locations to search for candidate distributions, in addition to those found in the registry indexes.

@@ -6855,9 +7116,16 @@ uv build [OPTIONS] [SRC]
--help, -h

Display the concise help for this command

+
--index index

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-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.

+

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:

@@ -6875,6 +7143,8 @@ uv build [OPTIONS] [SRC]

The index given by this flag is given lower priority than all other indexes specified via the --extra-index-url flag.

+

(Deprecated: use --default-index instead.)

+

May also be set with the UV_INDEX_URL environment variable.

--keyring-provider keyring-provider

Attempt to use keyring for authentication for index URLs.

diff --git a/docs/reference/settings.md b/docs/reference/settings.md index 4e05ccb40aa9..03e7dd2c3c8a 100644 --- a/docs/reference/settings.md +++ b/docs/reference/settings.md @@ -73,6 +73,50 @@ 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). + +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 front of the list of +the list of indexes, such that it is given the highest 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 +577,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 +638,66 @@ 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. + +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" } +``` + +Marking an index as `default = true` will disable the PyPI default index and move the +index to the end of the prioritized list, such that it is used when a package is not found +on any other 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 +732,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 +2012,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 db95f32ae44c..3c61340eea27 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 645ffc4582fb..7a1fe99242bd 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).\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 front of the list of the list of indexes, such that it is given the highest 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 enabled 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": [ { @@ -799,7 +842,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"