WIP: Proxy index

This commit is contained in:
John Mumm 2025-02-23 12:21:13 +01:00
parent 6e7ec3274a
commit 694f8b8d42
No known key found for this signature in database
GPG key ID: 08EFBF1E7471E2CF
20 changed files with 311 additions and 53 deletions

View file

@ -14,12 +14,15 @@ use uv_configuration::{
ConfigSettingEntry, ExportFormat, IndexStrategy, KeyringProviderType, PackageNameSpecifier,
ProjectBuildBackend, TargetTriple, TrustedHost, TrustedPublishing, VersionControlSystem,
};
use uv_distribution_types::{Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex};
use uv_distribution_types::{
Index, IndexUrl, Origin, PipExtraIndex, PipFindLinks, PipIndex, ProxyUrlFragment,
};
use uv_normalize::{ExtraName, GroupName, PackageName};
use uv_pep508::Requirement;
use uv_pypi_types::VerbatimParsedUrl;
use uv_python::{PythonDownloads, PythonPreference, PythonVersion};
use uv_resolver::{AnnotationStyle, ExcludeNewer, ForkStrategy, PrereleaseMode, ResolutionMode};
use uv_settings::Combine;
use uv_static::EnvVars;
pub mod comma;
@ -901,6 +904,18 @@ fn parse_default_index(input: &str) -> Result<Maybe<Index>, String> {
}
}
/// Parse a `--proxy-url` argument into an [`ProxyUrl`].
fn parse_proxy_url(input: &str) -> Result<Maybe<ProxyUrlFragment>, String> {
if input.is_empty() {
Ok(Maybe::None)
} else {
match ProxyUrlFragment::from_str(input) {
Ok(proxy_url) => Ok(Maybe::Some(proxy_url)),
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<Maybe<TrustedHost>, String> {
if input.is_empty() {
@ -4738,6 +4753,17 @@ pub struct IndexArgs {
#[arg(long, env = EnvVars::UV_EXTRA_INDEX_URL, value_delimiter = ' ', value_parser = parse_extra_index_url, help_heading = "Index options")]
pub extra_index_url: Option<Vec<Maybe<PipExtraIndex>>>,
/// 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 = EnvVars::UV_PRIVATE_PROXY_URL, value_delimiter = ' ', value_parser = parse_proxy_url, help_heading = "Index options")]
pub proxy_url: Option<Vec<Maybe<ProxyUrlFragment>>>,
/// Locations to search for candidate distributions, in addition to those found in the registry
/// indexes.
///
@ -4762,6 +4788,20 @@ pub struct IndexArgs {
pub no_index: bool,
}
impl IndexArgs {
pub fn combined_index(&self) -> Option<Vec<Index>> {
self.default_index
.clone()
.and_then(Maybe::into_option)
.map(|default_index| vec![default_index])
.combine(
self.index
.clone()
.map(|index| index.into_iter().filter_map(Maybe::into_option).collect()),
)
}
}
#[derive(Args)]
#[allow(clippy::struct_excessive_bools)]
pub struct RefreshArgs {

View file

@ -191,6 +191,7 @@ impl From<IndexArgs> for PipOptions {
index,
index_url,
extra_index_url,
proxy_url: _,
no_index,
find_links,
} = args;
@ -255,16 +256,31 @@ pub fn resolver_options(
no_binary_package,
} = build_args;
// let mut maybe_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()),
// );
// // Replace any proxied index with its proxy.
// if let Some(indexes) = &mut maybe_index {
// if let Some(proxies) = index_args.proxy_url {
// let proxies: Vec<ProxyUrl> = proxies.iter().filter_map(|proxy| Maybe::into_option(proxy.clone())).collect();
// for index in indexes.iter_mut() {
// if let Some(proxy) = proxies.iter().find(|proxy| index.name.as_ref().map_or(false, |name| name == &proxy.name)) {
// index.url = proxy.url.clone();
// index.origin = Some(Origin::Cli);
// }
// }
// }
// }
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: index_args.combined_index(),
index_url: index_args.index_url.and_then(Maybe::into_option),
extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
extra_index_url
@ -272,6 +288,9 @@ pub fn resolver_options(
.filter_map(Maybe::into_option)
.collect()
}),
proxy_urls: index_args
.proxy_url
.map(|proxies| proxies.into_iter().filter_map(Maybe::into_option).collect()),
no_index: if index_args.no_index {
Some(true)
} else {
@ -348,16 +367,8 @@ 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: index_args.combined_index(),
index_url: index_args.index_url.and_then(Maybe::into_option),
extra_index_url: index_args.extra_index_url.map(|extra_index_url| {
extra_index_url
@ -365,6 +376,9 @@ pub fn resolver_installer_options(
.filter_map(Maybe::into_option)
.collect()
}),
proxy_urls: index_args
.proxy_url
.map(|proxies| proxies.into_iter().filter_map(Maybe::into_option).collect()),
no_index: if index_args.no_index {
Some(true)
} else {

View file

@ -246,7 +246,7 @@ impl RegistryClient {
let indexes = if let Some(index) = index {
Either::Left(std::iter::once(index))
} else {
Either::Right(self.index_urls.indexes().map(Index::url))
Either::Right(self.index_urls.indexes().map(Index::proxy_or_url))
};
let mut it = indexes.peekable();

View file

@ -8,7 +8,7 @@ use uv_auth::Credentials;
use crate::index_name::{IndexName, IndexNameError};
use crate::origin::Origin;
use crate::{IndexUrl, IndexUrlError};
use crate::{IndexUrl, IndexUrlError, PROXY_URL_PATTERN};
#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
@ -82,6 +82,10 @@ pub struct Index {
/// publish-url = "https://upload.pypi.org/legacy/"
/// ```
pub publish_url: Option<Url>,
/// TODO !@: Document.
pub proxy_template: Option<String>,
/// TODO !@: Document
pub proxy_url: Option<IndexUrl>,
}
// #[derive(
@ -106,6 +110,8 @@ impl Index {
default: true,
origin: None,
publish_url: None,
proxy_template: None,
proxy_url: None,
}
}
@ -118,6 +124,8 @@ impl Index {
default: false,
origin: None,
publish_url: None,
proxy_template: None,
proxy_url: None,
}
}
@ -130,6 +138,8 @@ impl Index {
default: false,
origin: None,
publish_url: None,
proxy_template: None,
proxy_url: None,
}
}
@ -145,6 +155,15 @@ impl Index {
&self.url
}
/// Return the [`IndexUrl`] of the index, preferring a proxy if it exists.
pub fn proxy_or_url(&self) -> &IndexUrl {
if let Some(ref proxy_url) = self.proxy_url {
&proxy_url
} else {
&self.url
}
}
/// Consume the [`Index`] and return the [`IndexUrl`].
pub fn into_url(self) -> IndexUrl {
self.url
@ -189,6 +208,21 @@ impl Index {
Credentials::from_url(self.url.url())
}
/// TODO !@ Document
pub fn apply_proxy_template(
&mut self,
proxy_url_replacer: &str,
) -> Result<(), ProxyIndexSourceError> {
self.proxy_url = Some(self
.proxy_template
.as_ref()
.map(|template| template.replace(PROXY_URL_PATTERN, proxy_url_replacer))
.map(|url| IndexUrl::from_str(&url))
.transpose()?
.ok_or(ProxyIndexSourceError::MissingTemplate)?);
Ok(())
}
/// Resolve the index relative to the given root directory.
pub fn relative_to(mut self, root_dir: &Path) -> Result<Self, IndexUrlError> {
if let IndexUrl::Path(ref url) = self.url {
@ -216,6 +250,8 @@ impl FromStr for Index {
default: false,
origin: None,
publish_url: None,
proxy_template: None,
proxy_url: None,
});
}
}
@ -229,6 +265,8 @@ impl FromStr for Index {
default: false,
origin: None,
publish_url: None,
proxy_template: None,
proxy_url: None,
})
}
}
@ -243,3 +281,40 @@ pub enum IndexSourceError {
#[error("Index included a name, but the name was empty")]
EmptyName,
}
/// TODO !@: Document
#[derive(Debug, Clone, Hash, Eq, PartialEq, serde::Serialize, serde::Deserialize)]
#[cfg_attr(feature = "schemars", derive(schemars::JsonSchema))]
pub struct ProxyUrlFragment {
pub name: IndexName,
pub url_fragment: String,
}
impl FromStr for ProxyUrlFragment {
type Err = ProxyIndexSourceError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if let Some((name, url_fragment)) = s.split_once('=') {
// TODO !@ Check this case
//assert !name.chars().any(|c| c == ':') {
let name = IndexName::from_str(name)?;
Ok(Self { name, url_fragment: url_fragment.to_string() })
} else {
Err(ProxyIndexSourceError::MissingName)
}
}
}
/// An error that can occur when parsing an [`Index`].
#[derive(Error, Debug)]
pub enum ProxyIndexSourceError {
#[error(transparent)]
Url(#[from] IndexUrlError),
#[error(transparent)]
IndexName(#[from] IndexNameError),
#[error("Proxy index requires a name, but the name was empty")]
MissingName,
#[error("Proxy index requires a template in pyproject.toml, but there was none")]
MissingTemplate,
}

View file

@ -12,7 +12,7 @@ use url::{ParseError, Url};
use uv_pep508::{split_scheme, Scheme, VerbatimUrl, VerbatimUrlError};
use crate::{Index, Verbatim};
use crate::{Index, Origin, ProxyUrlFragment, Verbatim};
static PYPI_URL: LazyLock<Url> = LazyLock::new(|| Url::parse("https://pypi.org/simple").unwrap());
@ -22,6 +22,8 @@ static DEFAULT_INDEX: LazyLock<Index> = LazyLock::new(|| {
))))
});
pub static PROXY_URL_PATTERN: &str = "<omitted>";
/// The URL of an index to use for fetching packages (e.g., PyPI).
#[derive(Debug, Clone, Hash, Eq, PartialEq, Ord, PartialOrd)]
pub enum IndexUrl {
@ -237,7 +239,23 @@ pub struct IndexLocations {
impl IndexLocations {
/// Determine the index URLs to use for fetching packages.
pub fn new(indexes: Vec<Index>, flat_index: Vec<Index>, no_index: bool) -> Self {
pub fn new(
mut indexes: Vec<Index>,
flat_index: Vec<Index>,
proxies: &[ProxyUrlFragment],
no_index: bool,
) -> Self {
for index in &mut indexes {
if let Some(proxy) = proxies
.iter()
.find(|proxy| index.name.as_ref() == Some(&proxy.name))
{
// TODO !@ ideally we validate this ahead of time so we don't need a Result
index.apply_proxy_template(&proxy.url_fragment).unwrap();
index.origin = Some(Origin::Cli);
}
}
Self {
indexes,
flat_index,

View file

@ -35,7 +35,7 @@ macro_rules! impl_index {
where
S: serde::Serializer,
{
self.0.url().serialize(serializer)
self.0.proxy_or_url().serialize(serializer)
}
}

View file

@ -91,20 +91,20 @@ impl<'a> RegistryWheelIndex<'a> {
let mut seen = FxHashSet::default();
for index in index_locations.allowed_indexes() {
if !seen.insert(index.url()) {
if !seen.insert(index.proxy_or_url()) {
continue;
}
// 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.as_ref()),
WheelCache::Index(index.proxy_or_url()).wheel_dir(package.as_ref()),
);
// For registry wheels, the cache structure is: `<index>/<package-name>/<wheel>.http`
// or `<index>/<package-name>/<version>/<wheel>.rev`.
for file in files(&wheel_dir) {
match index.url() {
match index.proxy_or_url() {
// Add files from remote registries.
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
if file
@ -166,7 +166,7 @@ impl<'a> RegistryWheelIndex<'a> {
// from the registry.
let cache_shard = cache.shard(
CacheBucket::SourceDistributions,
WheelCache::Index(index.url()).wheel_dir(package.as_ref()),
WheelCache::Index(index.proxy_or_url()).wheel_dir(package.as_ref()),
);
// For registry source distributions, the cache structure is: `<index>/<package-name>/<version>/`.
@ -174,7 +174,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.proxy_or_url() {
// Add files from remote registries.
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
let revision_entry = cache_shard.entry(HTTP_REVISION);

View file

@ -221,7 +221,12 @@ impl LoweredRequirement {
.find(|Index { name, .. }| {
name.as_ref().is_some_and(|name| *name == index)
})
.map(|Index { url: index, .. }| index.clone())
.map(
|Index {
url: index,
..
}| index.clone(),
)
else {
return Err(LoweringError::MissingIndex(
requirement.name.clone(),
@ -442,7 +447,12 @@ impl LoweredRequirement {
.find(|Index { name, .. }| {
name.as_ref().is_some_and(|name| *name == index)
})
.map(|Index { url: index, .. }| index.clone())
.map(
|Index {
url: index,
..
}| index.clone(),
)
else {
return Err(LoweringError::MissingIndex(
requirement.name.clone(),

View file

@ -118,7 +118,7 @@ impl<'a> Planner<'a> {
match dist.as_ref() {
Dist::Built(BuiltDist::Registry(wheel)) => {
if let Some(distribution) = registry_index.get(wheel.name()).find_map(|entry| {
if *entry.index.url() != wheel.best_wheel().index {
if *entry.index.proxy_or_url() != wheel.best_wheel().index {
return None;
}
if entry.dist.filename.version != wheel.best_wheel().filename.version {
@ -238,7 +238,7 @@ impl<'a> Planner<'a> {
}
Dist::Source(SourceDist::Registry(sdist)) => {
if let Some(distribution) = registry_index.get(sdist.name()).find_map(|entry| {
if *entry.index.url() != sdist.index {
if *entry.index.proxy_or_url() != sdist.index {
return None;
}
if entry.dist.filename.name != sdist.name {

View file

@ -1295,9 +1295,9 @@ impl Lock {
locations
.allowed_indexes()
.into_iter()
.filter_map(|index| match index.url() {
.filter_map(|index| match index.proxy_or_url() {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => {
Some(UrlString::from(index.url().redacted().as_ref()))
Some(UrlString::from(index.proxy_or_url().redacted().as_ref()))
}
IndexUrl::Path(_) => None,
})
@ -1308,7 +1308,7 @@ impl Lock {
locations
.allowed_indexes()
.into_iter()
.filter_map(|index| match index.url() {
.filter_map(|index| match index.proxy_or_url() {
IndexUrl::Pypi(_) | IndexUrl::Url(_) => None,
IndexUrl::Path(url) => {
let path = url.to_file_path().ok()?;

View file

@ -900,7 +900,7 @@ impl PubGrubReportFormatter<'_> {
// indexes were not queried, and could contain a compatible version.
if let Some(next_index) = index_locations
.indexes()
.map(Index::url)
.map(Index::proxy_or_url)
.skip_while(|url| *url != found_index)
.nth(1)
{
@ -916,20 +916,20 @@ impl PubGrubReportFormatter<'_> {
// Add hints due to an index returning an unauthorized response.
for index in index_locations.allowed_indexes() {
if index_capabilities.unauthorized(&index.url) {
if index_capabilities.unauthorized(&index.proxy_or_url()) {
hints.insert(PubGrubHint::UnauthorizedIndex {
index: index.url.clone(),
index: index.proxy_or_url().clone(),
});
}
if index_capabilities.forbidden(&index.url) {
if index_capabilities.forbidden(&index.proxy_or_url()) {
// If the index is a PyTorch index (e.g., `https://download.pytorch.org/whl/cu118`),
// avoid noting the lack of credentials. PyTorch returns a 403 (Forbidden) status
// code for any package that does not exist.
if index.url.url().host_str() == Some("download.pytorch.org") {
if index.proxy_or_url().url().host_str() == Some("download.pytorch.org") {
continue;
}
hints.insert(PubGrubHint::ForbiddenIndex {
index: index.url.clone(),
index: index.proxy_or_url().clone(),
});
}
}

View file

@ -9,7 +9,7 @@ use uv_configuration::{
TargetTriple, TrustedHost, TrustedPublishing,
};
use uv_distribution_types::{
Index, IndexUrl, IndexUrlError, PipExtraIndex, PipFindLinks, PipIndex, StaticMetadata,
Index, IndexUrl, IndexUrlError, PipExtraIndex, PipFindLinks, PipIndex, ProxyUrlFragment, StaticMetadata,
};
use uv_install_wheel::LinkMode;
use uv_macros::{CombineOptions, OptionsMetadata};
@ -339,6 +339,7 @@ pub struct ResolverOptions {
pub index: Option<Vec<Index>>,
pub index_url: Option<PipIndex>,
pub extra_index_url: Option<Vec<PipExtraIndex>>,
pub proxy_urls: Option<Vec<ProxyUrlFragment>>,
pub no_index: Option<bool>,
pub find_links: Option<Vec<PipFindLinks>>,
pub index_strategy: Option<IndexStrategy>,
@ -442,6 +443,8 @@ pub struct ResolverInstallerOptions {
"#
)]
pub extra_index_url: Option<Vec<PipExtraIndex>>,
/// TODO !@ Document
pub proxy_urls: Option<Vec<ProxyUrlFragment>>,
/// Ignore all registry indexes (e.g., PyPI), instead relying on direct URL dependencies and
/// those provided via `--find-links`.
#[option(
@ -1581,6 +1584,7 @@ impl From<ResolverInstallerOptions> for ResolverOptions {
index: value.index,
index_url: value.index_url,
extra_index_url: value.extra_index_url,
proxy_urls: value.proxy_urls,
no_index: value.no_index,
find_links: value.find_links,
index_strategy: value.index_strategy,
@ -1644,6 +1648,7 @@ pub struct ToolOptions {
pub index: Option<Vec<Index>>,
pub index_url: Option<PipIndex>,
pub extra_index_url: Option<Vec<PipExtraIndex>>,
pub proxy_urls: Option<Vec<ProxyUrlFragment>>,
pub no_index: Option<bool>,
pub find_links: Option<Vec<PipFindLinks>>,
pub index_strategy: Option<IndexStrategy>,
@ -1671,6 +1676,7 @@ impl From<ResolverInstallerOptions> for ToolOptions {
index: value.index,
index_url: value.index_url,
extra_index_url: value.extra_index_url,
proxy_urls: value.proxy_urls,
no_index: value.no_index,
find_links: value.find_links,
index_strategy: value.index_strategy,
@ -1700,6 +1706,7 @@ impl From<ToolOptions> for ResolverInstallerOptions {
index: value.index,
index_url: value.index_url,
extra_index_url: value.extra_index_url,
proxy_urls: value.proxy_urls,
no_index: value.no_index,
find_links: value.find_links,
index_strategy: value.index_strategy,
@ -1753,6 +1760,7 @@ pub struct OptionsWire {
extra_index_url: Option<Vec<PipExtraIndex>>,
no_index: Option<bool>,
find_links: Option<Vec<PipFindLinks>>,
proxy_url: Option<Vec<ProxyUrlFragment>>,
index_strategy: Option<IndexStrategy>,
keyring_provider: Option<KeyringProviderType>,
allow_insecure_host: Option<Vec<TrustedHost>>,
@ -1836,6 +1844,7 @@ impl From<OptionsWire> for Options {
extra_index_url,
no_index,
find_links,
proxy_url,
index_strategy,
keyring_provider,
allow_insecure_host,
@ -1899,6 +1908,7 @@ impl From<OptionsWire> for Options {
index,
index_url,
extra_index_url,
proxy_urls: proxy_url,
no_index,
find_links,
index_strategy,

View file

@ -27,6 +27,10 @@ impl EnvVars {
/// space-separated list of URLs as additional indexes when searching for packages.
pub const UV_INDEX: &'static str = "UV_INDEX";
/// Equivalent to the `--proxy-url` command-line argument. If set, uv will use the
/// URL for this proxy in place of the index with the same name.
pub const UV_PRIVATE_PROXY_URL: &'static str = "UV_PRIVATE_PROXY_URL";
/// Equivalent to the `--index-url` command-line argument. If set, uv will use this
/// URL as the default index when searching for packages.
/// (Deprecated: use `UV_DEFAULT_INDEX` instead.)

View file

@ -263,7 +263,7 @@ impl PyProjectTomlMut {
.and_then(|item| item.as_str())
.and_then(|url| Url::parse(url).ok())
.is_some_and(|url| {
CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url())
CanonicalUrl::new(&url) == CanonicalUrl::new(index.url().url())
})
{
return true;
@ -299,9 +299,9 @@ impl PyProjectTomlMut {
.get("url")
.and_then(|item| item.as_str())
.and_then(|url| Url::parse(url).ok())
.is_none_or(|url| CanonicalUrl::new(&url) != CanonicalUrl::new(index.url.url()))
.is_none_or(|url| CanonicalUrl::new(&url) != CanonicalUrl::new(index.url().url()))
{
let mut formatted = Formatted::new(index.url.redacted().to_string());
let mut formatted = Formatted::new(index.url().redacted().to_string());
if let Some(value) = table.get("url").and_then(Item::as_value) {
if let Some(prefix) = value.decor().prefix() {
formatted.decor_mut().set_prefix(prefix.clone());
@ -360,7 +360,7 @@ impl PyProjectTomlMut {
.get("url")
.and_then(|item| item.as_str())
.and_then(|url| Url::parse(url).ok())
.is_some_and(|url| CanonicalUrl::new(&url) == CanonicalUrl::new(index.url.url()))
.is_some_and(|url| CanonicalUrl::new(&url) == CanonicalUrl::new(index.url().url()))
{
return false;
}

View file

@ -492,12 +492,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.default_index() {
writeln!(writer, "--index-url {}", index.url().verbatim())?;
writeln!(writer, "--index-url {}", index.proxy_or_url().verbatim())?;
wrote_preamble = true;
}
let mut seen = FxHashSet::default();
for extra_index in index_locations.implicit_indexes() {
if seen.insert(extra_index.url()) {
if seen.insert(extra_index.proxy_or_url()) {
writeln!(writer, "--extra-index-url {}", extra_index.url().verbatim())?;
wrote_preamble = true;
}

View file

@ -2456,7 +2456,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.default_index().map(Index::url) != Some(index_url) {
if settings.index_locations.default_index().map(Index::proxy_or_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."
);
@ -2466,7 +2466,7 @@ fn warn_on_requirements_txt_setting(
if !settings
.index_locations
.implicit_indexes()
.any(|index| index.url() == extra_index_url)
.any(|index| index.proxy_or_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.`"

View file

@ -91,6 +91,7 @@ pub(crate) async fn publish(
let index_urls = IndexLocations::new(
vec![Index::from_index_url(index_url.clone())],
Vec::new(),
&Vec::new(),
false,
)
.index_urls();

View file

@ -1336,7 +1336,7 @@ async fn run(mut cli: Cli) -> Result<ExitStatus> {
.publish_url
.clone()
.with_context(|| format!("Index is missing a publish URL: `{index_name}`"))?;
let check_url = index.url.clone();
let check_url = index.proxy_or_url().clone();
(publish_url, Some(check_url))
} else {
(publish_url, check_url)

View file

@ -28,7 +28,7 @@ use uv_configuration::{
RequiredVersion, SourceStrategy, TargetTriple, TrustedHost, TrustedPublishing, Upgrade,
VersionControlSystem,
};
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl};
use uv_distribution_types::{DependencyMetadata, Index, IndexLocations, IndexUrl, ProxyUrlFragment};
use uv_install_wheel::LinkMode;
use uv_normalize::PackageName;
use uv_pep508::{ExtraName, RequirementOrigin};
@ -2449,6 +2449,11 @@ impl From<ResolverOptions> for ResolverSettings {
.flatten()
.map(Index::from)
.collect(),
&value
.proxy_urls
.into_iter()
.flatten()
.collect::<Vec<ProxyUrlFragment>>(),
value.no_index.unwrap_or_default(),
),
resolution: value.resolution.unwrap_or_default(),
@ -2586,6 +2591,11 @@ impl From<ResolverInstallerOptions> for ResolverInstallerSettings {
.flatten()
.map(Index::from)
.collect(),
&value
.proxy_urls
.into_iter()
.flatten()
.collect::<Vec<ProxyUrlFragment>>(),
value.no_index.unwrap_or_default(),
),
resolution: value.resolution.unwrap_or_default(),
@ -2753,6 +2763,7 @@ impl PipSettings {
index: top_level_index,
index_url: top_level_index_url,
extra_index_url: top_level_extra_index_url,
proxy_urls: _,
no_index: top_level_no_index,
find_links: top_level_find_links,
index_strategy: top_level_index_strategy,
@ -2823,6 +2834,7 @@ impl PipSettings {
.flatten()
.map(Index::from)
.collect(),
&Vec::new(),
args.no_index.combine(no_index).unwrap_or_default(),
),
extras: ExtrasSpecification::from_args(
@ -3100,6 +3112,7 @@ impl PublishSettings {
.chain(index_url.into_iter().map(Index::from))
.collect(),
Vec::new(),
&Vec::new(),
false,
),
}

View file

@ -10186,3 +10186,76 @@ fn add_unsupported_git_scheme() {
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
"###);
}
/// Install a package from an index that requires authentication
#[test]
fn add_with_proxy_url() -> anyhow::Result<()> {
let context = TestContext::new("3.12");
let pyproject_toml = context.temp_dir.child("pyproject.toml");
pyproject_toml.write_str(indoc! {r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = []
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[[tool.uv.index]]
name = "alpha"
url = "https://not-real.astral.sh"
proxy-template = "https://<omitted>/basic-auth/simple"
"#})?;
uv_snapshot!(context.filters(), context.add()
.arg("anyio>=4.3.0")
.arg("--proxy-url")
.arg("alpha=public:heron@pypi-proxy.fly.dev"), @r"
success: true
exit_code: 0
----- stdout -----
----- stderr -----
Resolved 4 packages in [TIME]
Prepared 4 packages in [TIME]
Installed 4 packages in [TIME]
+ anyio==4.3.0
+ idna==3.6
+ project==0.1.0 (from file://[TEMP_DIR]/)
+ sniffio==1.3.1
"
);
let pyproject_toml = context.read("pyproject.toml");
insta::with_settings!({
filters => context.filters(),
}, {
assert_snapshot!(
pyproject_toml, @r#"
[project]
name = "project"
version = "0.1.0"
requires-python = ">=3.12"
dependencies = [
"anyio>=4.3.0",
]
[build-system]
requires = ["setuptools>=42"]
build-backend = "setuptools.build_meta"
[[tool.uv.index]]
name = "alpha"
url = "https://not-real.astral.sh"
proxy-template = "https://<omitted>/basic-auth/simple"
"#
);
});
context.assert_command("import anyio").success();
Ok(())
}