mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
WIP: Proxy index
This commit is contained in:
parent
6e7ec3274a
commit
694f8b8d42
20 changed files with 311 additions and 53 deletions
|
@ -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 {
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -35,7 +35,7 @@ macro_rules! impl_index {
|
|||
where
|
||||
S: serde::Serializer,
|
||||
{
|
||||
self.0.url().serialize(serializer)
|
||||
self.0.proxy_or_url().serialize(serializer)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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(),
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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()?;
|
||||
|
|
|
@ -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(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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.)
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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.`"
|
||||
|
|
|
@ -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();
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
),
|
||||
}
|
||||
|
|
|
@ -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(())
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue