From 49290de3d4c8a690a7041f29b846b0bc94cd447b Mon Sep 17 00:00:00 2001 From: konstin Date: Tue, 18 Nov 2025 16:34:10 +0100 Subject: [PATCH] Don't use a global static as credentials cache --- Cargo.lock | 1 + crates/uv-auth/src/cache.rs | 24 +++++++- crates/uv-auth/src/lib.rs | 38 +----------- crates/uv-auth/src/middleware.rs | 35 ++++++----- crates/uv-build-frontend/Cargo.toml | 1 + crates/uv-build-frontend/src/lib.rs | 9 ++- crates/uv-client/src/base_client.rs | 33 ++++++++-- crates/uv-client/src/registry_client.rs | 36 +++++++++-- crates/uv-dispatch/src/lib.rs | 1 + crates/uv-distribution-types/src/index_url.rs | 21 ------- .../src/distribution_database.rs | 6 +- .../src/metadata/build_requires.rs | 17 +++++- .../src/metadata/dependency_groups.rs | 4 +- .../uv-distribution/src/metadata/lowering.rs | 8 ++- crates/uv-distribution/src/metadata/mod.rs | 4 +- .../src/metadata/requires_dist.rs | 18 +++++- crates/uv-distribution/src/source/mod.rs | 61 +++++++++++++++---- crates/uv-once-map/src/lib.rs | 8 +++ crates/uv-resolver/src/lock/mod.rs | 2 +- crates/uv/src/commands/pip/operations.rs | 1 + crates/uv/src/commands/project/add.rs | 1 + crates/uv/src/commands/project/lock.rs | 42 ++++++++++--- crates/uv/src/commands/project/lock_target.rs | 5 +- crates/uv/src/commands/project/mod.rs | 8 ++- crates/uv/src/commands/project/run.rs | 14 ++++- crates/uv/src/commands/project/sync.rs | 34 ++++++++--- 26 files changed, 301 insertions(+), 131 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 028184222f4fc..076c41d7ef32a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5666,6 +5666,7 @@ dependencies = [ "tokio", "toml_edit 0.23.7", "tracing", + "uv-auth", "uv-cache-key", "uv-configuration", "uv-distribution", diff --git a/crates/uv-auth/src/cache.rs b/crates/uv-auth/src/cache.rs index cf08a5e051c87..5dc9b552feb0b 100644 --- a/crates/uv-auth/src/cache.rs +++ b/crates/uv-auth/src/cache.rs @@ -11,8 +11,8 @@ use url::Url; use uv_once_map::OnceMap; use uv_redacted::DisplaySafeUrl; -use crate::Realm; use crate::credentials::{Authentication, Username}; +use crate::{Credentials, Realm}; type FxOnceMap = OnceMap>; @@ -33,6 +33,7 @@ impl Display for FetchUrl { } } +#[derive(Debug)] // All internal types are redacted. pub struct CredentialsCache { /// A cache per realm and username realms: RwLock>>, @@ -58,6 +59,27 @@ impl CredentialsCache { } } + /// Populate the global authentication store with credentials on a URL, if there are any. + /// + /// Returns `true` if the store was updated. + pub fn store_credentials_from_url(&self, url: &DisplaySafeUrl) -> bool { + if let Some(credentials) = Credentials::from_url(url) { + trace!("Caching credentials for {url}"); + self.insert(url, Arc::new(Authentication::from(credentials))); + true + } else { + false + } + } + + /// Populate the global authentication store with credentials on a URL, if there are any. + /// + /// Returns `true` if the store was updated. + pub fn store_credentials(&self, url: &DisplaySafeUrl, credentials: Credentials) { + trace!("Caching credentials for {url}"); + self.insert(url, Arc::new(Authentication::from(credentials))); + } + /// Return the credentials that should be used for a realm and username, if any. pub(crate) fn get_realm( &self, diff --git a/crates/uv-auth/src/lib.rs b/crates/uv-auth/src/lib.rs index e1fe92f17ab59..e21d6c135c7a6 100644 --- a/crates/uv-auth/src/lib.rs +++ b/crates/uv-auth/src/lib.rs @@ -1,12 +1,5 @@ -use std::sync::{Arc, LazyLock}; - -use tracing::trace; - -use uv_redacted::DisplaySafeUrl; - -use crate::credentials::Authentication; pub use access_token::AccessToken; -use cache::CredentialsCache; +pub use cache::CredentialsCache; pub use credentials::{Credentials, Username}; pub use index::{AuthPolicy, Index, Indexes}; pub use keyring::KeyringProvider; @@ -29,32 +22,3 @@ mod pyx; mod realm; mod service; mod store; - -// TODO(zanieb): Consider passing a cache explicitly throughout - -/// Global authentication cache for a uv invocation -/// -/// This is used to share credentials across uv clients. -pub(crate) static CREDENTIALS_CACHE: LazyLock = - LazyLock::new(CredentialsCache::default); - -/// Populate the global authentication store with credentials on a URL, if there are any. -/// -/// Returns `true` if the store was updated. -pub fn store_credentials_from_url(url: &DisplaySafeUrl) -> bool { - if let Some(credentials) = Credentials::from_url(url) { - trace!("Caching credentials for {url}"); - CREDENTIALS_CACHE.insert(url, Arc::new(Authentication::from(credentials))); - true - } else { - false - } -} - -/// Populate the global authentication store with credentials on a URL, if there are any. -/// -/// Returns `true` if the store was updated. -pub fn store_credentials(url: &DisplaySafeUrl, credentials: Credentials) { - trace!("Caching credentials for {url}"); - CREDENTIALS_CACHE.insert(url, Arc::new(Authentication::from(credentials))); -} diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index dde997b347f9b..a1fc2507faf44 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -17,7 +17,7 @@ use crate::credentials::Authentication; use crate::providers::{HuggingFaceProvider, S3EndpointProvider}; use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore}; use crate::{ - AccessToken, CREDENTIALS_CACHE, CredentialsCache, KeyringProvider, + AccessToken, CredentialsCache, KeyringProvider, cache::FetchUrl, credentials::{Credentials, Username}, index::{AuthPolicy, Indexes}, @@ -131,7 +131,8 @@ pub struct AuthMiddleware { netrc: NetrcMode, text_store: TextStoreMode, keyring: Option, - cache: Option, + /// Global authentication cache for a uv invocation to share credentials across uv clients. + cache: Arc, /// Auth policies for specific URLs. indexes: Indexes, /// Set all endpoints as needing authentication. We never try to send an @@ -146,13 +147,20 @@ pub struct AuthMiddleware { preview: Preview, } +impl Default for AuthMiddleware { + fn default() -> Self { + Self::new() + } +} + impl AuthMiddleware { pub fn new() -> Self { Self { netrc: NetrcMode::default(), text_store: TextStoreMode::default(), keyring: None, - cache: None, + // TODO(konsti): There shouldn't be a credential cache without that in the initializer. + cache: Arc::new(CredentialsCache::default()), indexes: Indexes::new(), only_authenticated: false, base_client: None, @@ -205,7 +213,14 @@ impl AuthMiddleware { /// Configure the [`CredentialsCache`] to use. #[must_use] pub fn with_cache(mut self, cache: CredentialsCache) -> Self { - self.cache = Some(cache); + self.cache = Arc::new(cache); + self + } + + /// Configure the [`CredentialsCache`] to use from an existing [`Arc`]. + #[must_use] + pub fn with_cache_arc(mut self, cache: Arc) -> Self { + self.cache = cache; self } @@ -238,17 +253,9 @@ impl AuthMiddleware { self } - /// Get the configured authentication store. - /// - /// If not set, the global store is used. + /// Global authentication cache for a uv invocation to share credentials across uv clients. fn cache(&self) -> &CredentialsCache { - self.cache.as_ref().unwrap_or(&CREDENTIALS_CACHE) - } -} - -impl Default for AuthMiddleware { - fn default() -> Self { - Self::new() + &self.cache } } diff --git a/crates/uv-build-frontend/Cargo.toml b/crates/uv-build-frontend/Cargo.toml index b6af465a788f8..3416d4b400ed0 100644 --- a/crates/uv-build-frontend/Cargo.toml +++ b/crates/uv-build-frontend/Cargo.toml @@ -16,6 +16,7 @@ doctest = false workspace = true [dependencies] +uv-auth = { workspace = true } uv-cache-key = { workspace = true } uv-configuration = { workspace = true } uv-distribution = { workspace = true } diff --git a/crates/uv-build-frontend/src/lib.rs b/crates/uv-build-frontend/src/lib.rs index c081257664ffb..587bca9bb02fc 100644 --- a/crates/uv-build-frontend/src/lib.rs +++ b/crates/uv-build-frontend/src/lib.rs @@ -28,7 +28,7 @@ use tokio::io::AsyncBufReadExt; use tokio::process::Command; use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument, warn}; - +use uv_auth::CredentialsCache; use uv_cache_key::cache_digest; use uv_configuration::{BuildKind, BuildOutput, SourceStrategy}; use uv_distribution::BuildRequires; @@ -292,6 +292,7 @@ impl SourceBuild { mut environment_variables: FxHashMap, level: BuildOutput, concurrent_builds: usize, + credentials_cache: &CredentialsCache, preview: Preview, ) -> Result { let temp_dir = build_context.cache().venv_dir()?; @@ -310,6 +311,7 @@ impl SourceBuild { locations, source_strategy, workspace_cache, + credentials_cache, ) .await .map_err(|err| *err)?; @@ -452,6 +454,7 @@ impl SourceBuild { &environment_variables, &modified_path, &temp_dir, + credentials_cache, ) .await?; } @@ -556,6 +559,7 @@ impl SourceBuild { locations: &IndexLocations, source_strategy: SourceStrategy, workspace_cache: &WorkspaceCache, + credentials_cache: &CredentialsCache, ) -> Result<(Pep517Backend, Option), Box> { match fs::read_to_string(source_tree.join("pyproject.toml")) { Ok(toml) => { @@ -584,6 +588,7 @@ impl SourceBuild { locations, source_strategy, workspace_cache, + credentials_cache, ) .await .map_err(Error::Lowering)?; @@ -956,6 +961,7 @@ async fn create_pep517_build_environment( environment_variables: &FxHashMap, modified_path: &OsString, temp_dir: &TempDir, + credentials_cache: &CredentialsCache, ) -> Result<(), Error> { // Write the hook output to a file so that we can read it back reliably. let outfile = temp_dir @@ -1050,6 +1056,7 @@ async fn create_pep517_build_environment( locations, source_strategy, workspace_cache, + credentials_cache, ) .await .map_err(Error::Lowering)?; diff --git a/crates/uv-client/src/base_client.rs b/crates/uv-client/src/base_client.rs index 50151487ad604..33cd9e0d63eb1 100644 --- a/crates/uv-client/src/base_client.rs +++ b/crates/uv-client/src/base_client.rs @@ -28,7 +28,7 @@ use tracing::{debug, trace}; use url::ParseError; use url::Url; -use uv_auth::{AuthMiddleware, Credentials, Indexes, PyxTokenStore}; +use uv_auth::{AuthMiddleware, Credentials, CredentialsCache, Indexes, PyxTokenStore}; use uv_configuration::{KeyringProviderType, TrustedHost}; use uv_fs::Simplified; use uv_pep508::MarkerEnvironment; @@ -78,6 +78,8 @@ pub struct BaseClientBuilder<'a> { markers: Option<&'a MarkerEnvironment>, platform: Option<&'a Platform>, auth_integration: AuthIntegration, + /// Global authentication cache for a uv invocation to share credentials across uv clients. + credentials_cache: Arc, indexes: Indexes, timeout: Duration, extra_middleware: Option, @@ -138,6 +140,7 @@ impl Default for BaseClientBuilder<'_> { markers: None, platform: None, auth_integration: AuthIntegration::default(), + credentials_cache: Arc::new(CredentialsCache::default()), indexes: Indexes::new(), timeout: Duration::from_secs(30), extra_middleware: None, @@ -150,7 +153,7 @@ impl Default for BaseClientBuilder<'_> { } } -impl BaseClientBuilder<'_> { +impl<'a> BaseClientBuilder<'a> { pub fn new( connectivity: Connectivity, native_tls: bool, @@ -169,9 +172,7 @@ impl BaseClientBuilder<'_> { ..Self::default() } } -} -impl<'a> BaseClientBuilder<'a> { /// Use a custom reqwest client instead of creating a new one. /// /// This allows you to provide your own reqwest client with custom configuration. @@ -285,6 +286,20 @@ impl<'a> BaseClientBuilder<'a> { self } + pub fn credentials_cache(&self) -> &CredentialsCache { + &self.credentials_cache + } + + /// See [`CredentialsCache::store_credentials_from_url`]. + pub fn store_credentials_from_url(&self, url: &DisplaySafeUrl) -> bool { + self.credentials_cache.store_credentials_from_url(url) + } + + /// See [`CredentialsCache::store_credentials`]. + pub fn store_credentials(&self, url: &DisplaySafeUrl, credentials: Credentials) { + self.credentials_cache.store_credentials(url, credentials); + } + pub fn is_native_tls(&self) -> bool { self.native_tls } @@ -333,6 +348,7 @@ impl<'a> BaseClientBuilder<'a> { dangerous_client, raw_dangerous_client, timeout, + credentials_cache: self.credentials_cache.clone(), } } @@ -359,6 +375,7 @@ impl<'a> BaseClientBuilder<'a> { raw_client: existing.raw_client.clone(), raw_dangerous_client: existing.raw_dangerous_client.clone(), timeout: existing.timeout, + credentials_cache: existing.credentials_cache.clone(), } } @@ -563,6 +580,7 @@ impl<'a> BaseClientBuilder<'a> { match self.auth_integration { AuthIntegration::Default => { let mut auth_middleware = AuthMiddleware::new() + .with_cache_arc(self.credentials_cache.clone()) .with_base_client(base_client) .with_indexes(self.indexes.clone()) .with_keyring(self.keyring.to_provider()) @@ -574,6 +592,7 @@ impl<'a> BaseClientBuilder<'a> { } AuthIntegration::OnlyAuthenticated => { let mut auth_middleware = AuthMiddleware::new() + .with_cache_arc(self.credentials_cache.clone()) .with_base_client(base_client) .with_indexes(self.indexes.clone()) .with_keyring(self.keyring.to_provider()) @@ -617,6 +636,8 @@ pub struct BaseClient { allow_insecure_host: Vec, /// The number of retries to attempt on transient errors. retries: u32, + /// Global authentication cache for a uv invocation to share credentials across uv clients. + credentials_cache: Arc, } #[derive(Debug, Clone, Copy)] @@ -668,6 +689,10 @@ impl BaseClient { } builder.build_with_max_retries(self.retries) } + + pub fn credentials_cache(&self) -> &CredentialsCache { + &self.credentials_cache + } } /// Wrapper around [`ClientWithMiddleware`] that manages redirects. diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 79b6025f2bc2f..0c392a4c28485 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -15,7 +15,7 @@ use tokio::sync::{Mutex, Semaphore}; use tracing::{Instrument, debug, info_span, instrument, trace, warn}; use url::Url; -use uv_auth::{Indexes, PyxTokenStore}; +use uv_auth::{CredentialsCache, Indexes, PyxTokenStore}; use uv_cache::{Cache, CacheBucket, CacheEntry, WheelCache}; use uv_configuration::IndexStrategy; use uv_configuration::KeyringProviderType; @@ -148,8 +148,30 @@ impl<'a> RegistryClientBuilder<'a> { self } - pub fn build(self) -> RegistryClient { - self.index_locations.cache_index_credentials(); + /// Add all authenticated sources to the cache. + pub fn cache_index_credentials(&mut self) { + for index in self.index_locations.known_indexes() { + if let Some(credentials) = index.credentials() { + trace!( + "Read credentials for index {}", + index + .name + .as_ref() + .map(ToString::to_string) + .unwrap_or_else(|| index.url.to_string()) + ); + if let Some(root_url) = index.root_url() { + self.base_client_builder + .store_credentials(&root_url, credentials.clone()); + } + self.base_client_builder + .store_credentials(index.raw_url(), credentials); + } + } + } + + pub fn build(mut self) -> RegistryClient { + self.cache_index_credentials(); let index_urls = self.index_locations.index_urls(); // Build a base client @@ -180,8 +202,8 @@ impl<'a> RegistryClientBuilder<'a> { } /// Share the underlying client between two different middleware configurations. - pub fn wrap_existing(self, existing: &BaseClient) -> RegistryClient { - self.index_locations.cache_index_credentials(); + pub fn wrap_existing(mut self, existing: &BaseClient) -> RegistryClient { + self.cache_index_credentials(); let index_urls = self.index_locations.index_urls(); // Wrap in any relevant middleware and handle connectivity. @@ -269,6 +291,10 @@ impl RegistryClient { self.timeout } + pub fn credentials_cache(&self) -> &CredentialsCache { + self.client.uncached().credentials_cache() + } + /// Return the appropriate index URLs for the given [`PackageName`]. fn index_urls_for( &self, diff --git a/crates/uv-dispatch/src/lib.rs b/crates/uv-dispatch/src/lib.rs index f148e6dd24024..afcf38bd16ce2 100644 --- a/crates/uv-dispatch/src/lib.rs +++ b/crates/uv-dispatch/src/lib.rs @@ -492,6 +492,7 @@ impl BuildContext for BuildDispatch<'_> { environment_variables, build_output, self.concurrency.builds, + self.client.credentials_cache(), self.preview, ) .boxed_local() diff --git a/crates/uv-distribution-types/src/index_url.rs b/crates/uv-distribution-types/src/index_url.rs index a7797d3b2644c..2d8603799cee7 100644 --- a/crates/uv-distribution-types/src/index_url.rs +++ b/crates/uv-distribution-types/src/index_url.rs @@ -8,7 +8,6 @@ use std::sync::{Arc, LazyLock, RwLock}; use itertools::Either; use rustc_hash::{FxHashMap, FxHashSet}; use thiserror::Error; -use tracing::trace; use url::{ParseError, Url}; use uv_auth::RealmRef; use uv_cache_key::CanonicalUrl; @@ -440,26 +439,6 @@ impl<'a> IndexLocations { } } - /// Add all authenticated sources to the cache. - pub fn cache_index_credentials(&self) { - for index in self.known_indexes() { - if let Some(credentials) = index.credentials() { - trace!( - "Read credentials for index {}", - index - .name - .as_ref() - .map(ToString::to_string) - .unwrap_or_else(|| index.url.to_string()) - ); - if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); - } - uv_auth::store_credentials(index.raw_url(), credentials); - } - } - } - /// Return the Simple API cache control header for an [`IndexUrl`], if configured. pub fn simple_api_cache_control_for(&self, url: &IndexUrl) -> Option<&str> { for index in &self.indexes { diff --git a/crates/uv-distribution/src/distribution_database.rs b/crates/uv-distribution/src/distribution_database.rs index ef2227df6152a..805c59b260d62 100644 --- a/crates/uv-distribution/src/distribution_database.rs +++ b/crates/uv-distribution/src/distribution_database.rs @@ -554,7 +554,11 @@ impl<'a, Context: BuildContext> DistributionDatabase<'a, Context> { pyproject_toml: &PyProjectToml, ) -> Result, Error> { self.builder - .source_tree_requires_dist(path, pyproject_toml) + .source_tree_requires_dist( + path, + pyproject_toml, + self.client.unmanaged.credentials_cache(), + ) .await } diff --git a/crates/uv-distribution/src/metadata/build_requires.rs b/crates/uv-distribution/src/metadata/build_requires.rs index 723ca4f96501a..fa21aa0413377 100644 --- a/crates/uv-distribution/src/metadata/build_requires.rs +++ b/crates/uv-distribution/src/metadata/build_requires.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; use std::path::Path; - +use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::{ ExtraBuildRequirement, ExtraBuildRequires, IndexLocations, Requirement, @@ -42,6 +42,7 @@ impl BuildRequires { locations: &IndexLocations, sources: SourceStrategy, cache: &WorkspaceCache, + credentials_cache: &CredentialsCache, ) -> Result { let discovery = match sources { SourceStrategy::Enabled => DiscoveryOptions::default(), @@ -56,7 +57,13 @@ impl BuildRequires { return Ok(Self::from_metadata23(metadata)); }; - Self::from_project_workspace(metadata, &project_workspace, locations, sources) + Self::from_project_workspace( + metadata, + &project_workspace, + locations, + sources, + credentials_cache, + ) } /// Lower the `build-system.requires` field from a `pyproject.toml` file. @@ -65,6 +72,7 @@ impl BuildRequires { project_workspace: &ProjectWorkspace, locations: &IndexLocations, source_strategy: SourceStrategy, + credentials_cache: &CredentialsCache, ) -> Result { // Collect any `tool.uv.index` entries. let empty = vec![]; @@ -114,6 +122,7 @@ impl BuildRequires { locations, project_workspace.workspace(), None, + credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), @@ -139,6 +148,7 @@ impl BuildRequires { workspace: &Workspace, locations: &IndexLocations, source_strategy: SourceStrategy, + credentials_cache: &CredentialsCache, ) -> Result { // Collect any `tool.uv.index` entries. let empty = vec![]; @@ -186,6 +196,7 @@ impl BuildRequires { locations, workspace, None, + credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), @@ -225,6 +236,7 @@ impl LoweredExtraBuildDependencies { workspace: &Workspace, index_locations: &IndexLocations, source_strategy: SourceStrategy, + credentials_cache: &CredentialsCache, ) -> Result { match source_strategy { SourceStrategy::Enabled => { @@ -271,6 +283,7 @@ impl LoweredExtraBuildDependencies { index_locations, workspace, None, + credentials_cache, ) .map(move |requirement| { match requirement { diff --git a/crates/uv-distribution/src/metadata/dependency_groups.rs b/crates/uv-distribution/src/metadata/dependency_groups.rs index d12e0651de5a7..058cf68db30d1 100644 --- a/crates/uv-distribution/src/metadata/dependency_groups.rs +++ b/crates/uv-distribution/src/metadata/dependency_groups.rs @@ -1,6 +1,6 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; - +use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::{IndexLocations, Requirement}; use uv_normalize::{GroupName, PackageName}; @@ -57,6 +57,7 @@ impl SourcedDependencyGroups { locations: &IndexLocations, source_strategy: SourceStrategy, cache: &WorkspaceCache, + credentials_cache: &CredentialsCache, ) -> Result { // If the `pyproject.toml` doesn't exist, fail early. if !pyproject_path.is_file() { @@ -156,6 +157,7 @@ impl SourcedDependencyGroups { locations, project.workspace(), git_member, + credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), diff --git a/crates/uv-distribution/src/metadata/lowering.rs b/crates/uv-distribution/src/metadata/lowering.rs index 63e43e6a342a6..8133c925d0431 100644 --- a/crates/uv-distribution/src/metadata/lowering.rs +++ b/crates/uv-distribution/src/metadata/lowering.rs @@ -4,7 +4,7 @@ use std::path::{Path, PathBuf}; use either::Either; use thiserror::Error; - +use uv_auth::CredentialsCache; use uv_distribution_filename::DistExtension; use uv_distribution_types::{ Index, IndexLocations, IndexMetadata, IndexName, Origin, Requirement, RequirementSource, @@ -44,6 +44,7 @@ impl LoweredRequirement { locations: &'data IndexLocations, workspace: &'data Workspace, git_member: Option<&'data GitWorkspaceMember<'data>>, + credentials_cache: &'data CredentialsCache, ) -> impl Iterator> + use<'data> + 'data { // Identify the source from the `tool.uv.sources` table. let (sources, origin) = if let Some(source) = project_sources.get(&requirement.name) { @@ -231,7 +232,7 @@ impl LoweredRequirement { )); }; if let Some(credentials) = index.credentials() { - uv_auth::store_credentials(index.raw_url(), credentials); + credentials_cache.store_credentials(index.raw_url(), credentials); } let index = IndexMetadata { url: index.url.clone(), @@ -358,6 +359,7 @@ impl LoweredRequirement { sources: &'data BTreeMap, indexes: &'data [Index], locations: &'data IndexLocations, + credentials_cache: &'data CredentialsCache, ) -> impl Iterator> + 'data { let source = sources.get(&requirement.name).cloned(); @@ -466,7 +468,7 @@ impl LoweredRequirement { )); }; if let Some(credentials) = index.credentials() { - uv_auth::store_credentials(index.raw_url(), credentials); + credentials_cache.store_credentials(index.raw_url(), credentials); } let index = IndexMetadata { url: index.url.clone(), diff --git a/crates/uv-distribution/src/metadata/mod.rs b/crates/uv-distribution/src/metadata/mod.rs index 3557de15e3f49..938791f8c104f 100644 --- a/crates/uv-distribution/src/metadata/mod.rs +++ b/crates/uv-distribution/src/metadata/mod.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use thiserror::Error; - +use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::{GitSourceUrl, IndexLocations, Requirement}; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -91,6 +91,7 @@ impl Metadata { locations: &IndexLocations, sources: SourceStrategy, cache: &WorkspaceCache, + credentials_cache: &CredentialsCache, ) -> Result { // Lower the requirements. let requires_dist = uv_pypi_types::RequiresDist { @@ -112,6 +113,7 @@ impl Metadata { locations, sources, cache, + credentials_cache, ) .await?; diff --git a/crates/uv-distribution/src/metadata/requires_dist.rs b/crates/uv-distribution/src/metadata/requires_dist.rs index f9482d977818b..2cbf4c6d65f9d 100644 --- a/crates/uv-distribution/src/metadata/requires_dist.rs +++ b/crates/uv-distribution/src/metadata/requires_dist.rs @@ -3,7 +3,7 @@ use std::path::Path; use std::slice; use rustc_hash::FxHashSet; - +use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::{IndexLocations, Requirement}; use uv_normalize::{ExtraName, GroupName, PackageName}; @@ -48,6 +48,7 @@ impl RequiresDist { locations: &IndexLocations, sources: SourceStrategy, cache: &WorkspaceCache, + credentials_cache: &CredentialsCache, ) -> Result { let discovery = DiscoveryOptions { stop_discovery_at: git_member.map(|git_member| { @@ -69,7 +70,14 @@ impl RequiresDist { return Ok(Self::from_metadata23(metadata)); }; - Self::from_project_workspace(metadata, &project_workspace, git_member, locations, sources) + Self::from_project_workspace( + metadata, + &project_workspace, + git_member, + locations, + sources, + credentials_cache, + ) } fn from_project_workspace( @@ -78,6 +86,7 @@ impl RequiresDist { git_member: Option<&GitWorkspaceMember<'_>>, locations: &IndexLocations, source_strategy: SourceStrategy, + credentials_cache: &CredentialsCache, ) -> Result { // Collect any `tool.uv.index` entries. let empty = vec![]; @@ -140,6 +149,7 @@ impl RequiresDist { locations, project_workspace.workspace(), git_member, + credentials_cache, ) .map( move |requirement| match requirement { @@ -182,6 +192,7 @@ impl RequiresDist { locations, project_workspace.workspace(), git_member, + credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), @@ -432,7 +443,7 @@ mod test { use anyhow::Context; use indoc::indoc; use insta::assert_snapshot; - + use uv_auth::CredentialsCache; use uv_configuration::SourceStrategy; use uv_distribution_types::IndexLocations; use uv_normalize::PackageName; @@ -468,6 +479,7 @@ mod test { None, &IndexLocations::default(), SourceStrategy::default(), + &CredentialsCache::new(), )?) } diff --git a/crates/uv-distribution/src/source/mod.rs b/crates/uv-distribution/src/source/mod.rs index e2ca8b46ebac1..a84eac7b80587 100644 --- a/crates/uv-distribution/src/source/mod.rs +++ b/crates/uv-distribution/src/source/mod.rs @@ -20,9 +20,9 @@ use reqwest::{Response, StatusCode}; use tokio_util::compat::FuturesAsyncReadCompatExt; use tracing::{Instrument, debug, info_span, instrument, warn}; use url::Url; -use uv_redacted::DisplaySafeUrl; use zip::ZipArchive; +use uv_auth::CredentialsCache; use uv_cache::{Cache, CacheBucket, CacheEntry, CacheShard, Removal, WheelCache}; use uv_cache_info::CacheInfo; use uv_client::{ @@ -44,6 +44,7 @@ use uv_normalize::PackageName; use uv_pep440::{Version, release_specifiers_to_ranges}; use uv_platform_tags::Tags; use uv_pypi_types::{HashAlgorithm, HashDigest, HashDigests, PyProjectToml, ResolutionMetadata}; +use uv_redacted::DisplaySafeUrl; use uv_types::{BuildContext, BuildKey, BuildStack, SourceBuildTrait}; use uv_workspace::pyproject::ToolUvSources; @@ -326,14 +327,25 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await? } BuildableSource::Dist(SourceDist::Git(dist)) => { - self.git_metadata(source, &GitSourceUrl::from(dist), hashes, client) - .boxed_local() - .await? + self.git_metadata( + source, + &GitSourceUrl::from(dist), + hashes, + client, + client.unmanaged.credentials_cache(), + ) + .boxed_local() + .await? } BuildableSource::Dist(SourceDist::Directory(dist)) => { - self.source_tree_metadata(source, &DirectorySourceUrl::from(dist), hashes) - .boxed_local() - .await? + self.source_tree_metadata( + source, + &DirectorySourceUrl::from(dist), + hashes, + client.unmanaged.credentials_cache(), + ) + .boxed_local() + .await? } BuildableSource::Dist(SourceDist::Path(dist)) => { let cache_shard = self.build_context.cache().shard( @@ -365,14 +377,25 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { .await? } BuildableSource::Url(SourceUrl::Git(resource)) => { - self.git_metadata(source, resource, hashes, client) - .boxed_local() - .await? + self.git_metadata( + source, + resource, + hashes, + client, + client.unmanaged.credentials_cache(), + ) + .boxed_local() + .await? } BuildableSource::Url(SourceUrl::Directory(resource)) => { - self.source_tree_metadata(source, resource, hashes) - .boxed_local() - .await? + self.source_tree_metadata( + source, + resource, + hashes, + client.unmanaged.credentials_cache(), + ) + .boxed_local() + .await? } BuildableSource::Url(SourceUrl::Path(resource)) => { let cache_shard = self.build_context.cache().shard( @@ -1249,6 +1272,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { source: &BuildableSource<'_>, resource: &DirectorySourceUrl<'_>, hashes: HashPolicy<'_>, + credentials_cache: &CredentialsCache, ) -> Result { // Before running the build, check that the hashes match. if hashes.is_validate() { @@ -1266,6 +1290,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?, )); @@ -1319,6 +1344,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?, )); @@ -1368,6 +1394,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?, )); @@ -1429,6 +1456,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?, )) @@ -1491,6 +1519,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { &self, path: &Path, pyproject_toml: &PyProjectToml, + credentials_cache: &CredentialsCache, ) -> Result, Error> { // Attempt to read static metadata from the `pyproject.toml`. match uv_pypi_types::RequiresDist::from_pyproject_toml(pyproject_toml.clone()) { @@ -1503,6 +1532,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?; Ok(Some(requires_dist)) @@ -1662,6 +1692,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { resource: &GitSourceUrl<'_>, hashes: HashPolicy<'_>, client: &ManagedClient<'_>, + credentials_cache: &CredentialsCache, ) -> Result { // Before running the build, check that the hashes match. if hashes.is_validate() { @@ -1823,6 +1854,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?, )); @@ -1856,6 +1888,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?, )); @@ -1908,6 +1941,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?, )); @@ -1969,6 +2003,7 @@ impl<'a, T: BuildContext> SourceDistributionBuilder<'a, T> { self.build_context.locations(), self.build_context.sources(), self.build_context.workspace_cache(), + credentials_cache, ) .await?, )) diff --git a/crates/uv-once-map/src/lib.rs b/crates/uv-once-map/src/lib.rs index caafc506f39eb..4d4e01b0e70cd 100644 --- a/crates/uv-once-map/src/lib.rs +++ b/crates/uv-once-map/src/lib.rs @@ -1,4 +1,5 @@ use std::borrow::Borrow; +use std::fmt::{Debug, Formatter}; use std::hash::{BuildHasher, Hash, RandomState}; use std::pin::pin; use std::sync::Arc; @@ -19,6 +20,12 @@ pub struct OnceMap { items: DashMap, S>, } +impl Debug for OnceMap { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + Debug::fmt(&self.items, f) + } +} + impl OnceMap { /// Create a [`OnceMap`] with the specified hasher. pub fn with_hasher(hasher: H) -> Self { @@ -142,6 +149,7 @@ where } } +#[derive(Debug)] enum Value { Waiting(Arc), Filled(V), diff --git a/crates/uv-resolver/src/lock/mod.rs b/crates/uv-resolver/src/lock/mod.rs index f4cdbb16cafba..4e841c87f40fc 100644 --- a/crates/uv-resolver/src/lock/mod.rs +++ b/crates/uv-resolver/src/lock/mod.rs @@ -1445,7 +1445,7 @@ impl Lock { Ok(SatisfiesResult::Satisfied) } - /// Convert the [`Lock`] to a [`Resolution`] using the given marker environment, tags, and root. + /// Check whether the lock matches the project structure, requirements and configuration. pub async fn satisfies( &self, root: &Path, diff --git a/crates/uv/src/commands/pip/operations.rs b/crates/uv/src/commands/pip/operations.rs index 11ccca2c2d5f7..01db57db0cbc0 100644 --- a/crates/uv/src/commands/pip/operations.rs +++ b/crates/uv/src/commands/pip/operations.rs @@ -217,6 +217,7 @@ pub(crate) async fn resolve( build_dispatch.locations(), build_dispatch.sources(), build_dispatch.workspace_cache(), + client.credentials_cache(), ) .await .with_context(|| { diff --git a/crates/uv/src/commands/project/add.rs b/crates/uv/src/commands/project/add.rs index a481841edf677..2ad83a0294192 100644 --- a/crates/uv/src/commands/project/add.rs +++ b/crates/uv/src/commands/project/add.rs @@ -442,6 +442,7 @@ pub(crate) async fn add( project.workspace(), &settings.resolver.index_locations, settings.resolver.sources, + client.credentials_cache(), )? } else { LoweredExtraBuildDependencies::from_non_lowered( diff --git a/crates/uv/src/commands/project/lock.rs b/crates/uv/src/commands/project/lock.rs index e90b05ca3efff..12843bb34fa64 100644 --- a/crates/uv/src/commands/project/lock.rs +++ b/crates/uv/src/commands/project/lock.rs @@ -494,14 +494,39 @@ async fn do_lock( let source_trees = vec![]; // If necessary, lower the overrides and constraints. - let requirements = target.lower(requirements, index_locations, *sources)?; - let overrides = target.lower(overrides, index_locations, *sources)?; - let constraints = target.lower(constraints, index_locations, *sources)?; - let build_constraints = target.lower(build_constraints, index_locations, *sources)?; + let requirements = target.lower( + requirements, + index_locations, + *sources, + client_builder.credentials_cache(), + )?; + let overrides = target.lower( + overrides, + index_locations, + *sources, + client_builder.credentials_cache(), + )?; + let constraints = target.lower( + constraints, + index_locations, + *sources, + client_builder.credentials_cache(), + )?; + let build_constraints = target.lower( + build_constraints, + index_locations, + *sources, + client_builder.credentials_cache(), + )?; let dependency_groups = dependency_groups .into_iter() .map(|(name, group)| { - let requirements = target.lower(group.requirements, index_locations, *sources)?; + let requirements = target.lower( + group.requirements, + index_locations, + *sources, + client_builder.credentials_cache(), + )?; Ok((name, requirements)) }) .collect::, ProjectError>>()?; @@ -655,9 +680,9 @@ async fn do_lock( for index in target.indexes() { if let Some(credentials) = index.credentials() { if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); + client_builder.store_credentials(&root_url, credentials.clone()); } - uv_auth::store_credentials(index.raw_url(), credentials); + client_builder.store_credentials(index.raw_url(), credentials); } } @@ -716,10 +741,11 @@ async fn do_lock( workspace, index_locations, *sources, + client.credentials_cache(), )?, LockTarget::Script(script) => { // Try to get extra build dependencies from the script metadata - script_extra_build_requires((*script).into(), settings)? + script_extra_build_requires((*script).into(), settings, client.credentials_cache())? } } .into_inner(); diff --git a/crates/uv/src/commands/project/lock_target.rs b/crates/uv/src/commands/project/lock_target.rs index b5fdc99a97db6..12d0abd9c6a54 100644 --- a/crates/uv/src/commands/project/lock_target.rs +++ b/crates/uv/src/commands/project/lock_target.rs @@ -2,7 +2,7 @@ use std::collections::BTreeMap; use std::path::{Path, PathBuf}; use itertools::Either; - +use uv_auth::CredentialsCache; use uv_configuration::{DependencyGroupsWithDefaults, SourceStrategy}; use uv_distribution::LoweredRequirement; use uv_distribution_types::{Index, IndexLocations, Requirement, RequiresPython}; @@ -343,6 +343,7 @@ impl<'lock> LockTarget<'lock> { requirements: Vec>, locations: &IndexLocations, sources: SourceStrategy, + credentials_cache: &CredentialsCache, ) -> Result, uv_distribution::MetadataError> { match self { Self::Workspace(workspace) => { @@ -362,6 +363,7 @@ impl<'lock> LockTarget<'lock> { workspace, locations, sources, + credentials_cache, )?; Ok(metadata @@ -407,6 +409,7 @@ impl<'lock> LockTarget<'lock> { sources, indexes, locations, + credentials_cache, ) .map(move |requirement| match requirement { Ok(requirement) => Ok(requirement.into_inner()), diff --git a/crates/uv/src/commands/project/mod.rs b/crates/uv/src/commands/project/mod.rs index 9f61445def78b..2571fe4120461 100644 --- a/crates/uv/src/commands/project/mod.rs +++ b/crates/uv/src/commands/project/mod.rs @@ -7,7 +7,7 @@ use std::sync::Arc; use itertools::Itertools; use owo_colors::OwoColorize; use tracing::{debug, trace, warn}; - +use uv_auth::CredentialsCache; use uv_cache::{Cache, CacheBucket}; use uv_cache_key::cache_digest; use uv_client::{BaseClientBuilder, FlatIndexClient, RegistryClientBuilder}; @@ -2569,6 +2569,7 @@ pub(crate) fn detect_conflicts( pub(crate) fn script_specification( script: Pep723ItemRef<'_>, settings: &ResolverSettings, + credentials_cache: &CredentialsCache, ) -> Result, ProjectError> { let Some(dependencies) = script.metadata().dependencies.as_ref() else { return Ok(None); @@ -2588,6 +2589,7 @@ pub(crate) fn script_specification( script_sources, script_indexes, &settings.index_locations, + credentials_cache, ) .map_ok(LoweredRequirement::into_inner) }) @@ -2608,6 +2610,7 @@ pub(crate) fn script_specification( script_sources, script_indexes, &settings.index_locations, + credentials_cache, ) .map_ok(LoweredRequirement::into_inner) }) @@ -2628,6 +2631,7 @@ pub(crate) fn script_specification( script_sources, script_indexes, &settings.index_locations, + credentials_cache, ) .map_ok(LoweredRequirement::into_inner) }) @@ -2645,6 +2649,7 @@ pub(crate) fn script_specification( pub(crate) fn script_extra_build_requires( script: Pep723ItemRef<'_>, settings: &ResolverSettings, + credentials_cache: &CredentialsCache, ) -> Result { let script_dir = script.directory()?; let script_indexes = script.indexes(settings.sources); @@ -2677,6 +2682,7 @@ pub(crate) fn script_extra_build_requires( script_sources, script_indexes, &settings.index_locations, + credentials_cache, ) .map_ok(move |requirement| ExtraBuildRequirement { requirement: requirement.into_inner(), diff --git a/crates/uv/src/commands/project/run.rs b/crates/uv/src/commands/project/run.rs index 16053de237eab..dc8b201d0f6e8 100644 --- a/crates/uv/src/commands/project/run.rs +++ b/crates/uv/src/commands/project/run.rs @@ -372,9 +372,17 @@ hint: If you are running a script with `{}` in the shebang, you may need to incl } // Install the script requirements, if necessary. Otherwise, use an isolated environment. - if let Some(spec) = script_specification((&script).into(), &settings.resolver)? { - let script_extra_build_requires = - script_extra_build_requires((&script).into(), &settings.resolver)?.into_inner(); + if let Some(spec) = script_specification( + (&script).into(), + &settings.resolver, + client_builder.credentials_cache(), + )? { + let script_extra_build_requires = script_extra_build_requires( + (&script).into(), + &settings.resolver, + client_builder.credentials_cache(), + )? + .into_inner(); let environment = ScriptEnvironment::get_or_init( (&script).into(), python.as_deref().map(PythonRequest::parse), diff --git a/crates/uv/src/commands/project/sync.rs b/crates/uv/src/commands/project/sync.rs index 6ee926daf01c7..2542965e8cd53 100644 --- a/crates/uv/src/commands/project/sync.rs +++ b/crates/uv/src/commands/project/sync.rs @@ -239,9 +239,18 @@ pub(crate) async fn sync( } // Parse the requirements from the script. - let spec = script_specification(script.into(), &settings.resolver)?.unwrap_or_default(); - let script_extra_build_requires = - script_extra_build_requires(script.into(), &settings.resolver)?.into_inner(); + let spec = script_specification( + script.into(), + &settings.resolver, + client_builder.credentials_cache(), + )? + .unwrap_or_default(); + let script_extra_build_requires = script_extra_build_requires( + script.into(), + &settings.resolver, + client_builder.credentials_cache(), + )? + .into_inner(); // Parse the build constraints from the script. let build_constraints = script @@ -645,6 +654,7 @@ pub(super) async fn do_sync( workspace, index_locations, sources, + client_builder.credentials_cache(), )? } InstallTarget::Script { script, .. } => { @@ -668,7 +678,11 @@ pub(super) async fn do_sync( sources, upgrade: Upgrade::default(), }; - script_extra_build_requires((*script).into(), &resolver_settings)? + script_extra_build_requires( + (*script).into(), + &resolver_settings, + client_builder.credentials_cache(), + )? } } .into_inner(); @@ -744,7 +758,7 @@ pub(super) async fn do_sync( let extra_build_requires = extra_build_requires.match_runtime(&resolution)?; // Populate credentials from the target. - store_credentials_from_target(target); + store_credentials_from_target(target, &client_builder); // Initialize the registry client. let client = RegistryClientBuilder::new(client_builder, cache.clone()) @@ -928,14 +942,14 @@ fn apply_editable_mode(resolution: Resolution, editable: Option) - /// /// These credentials can come from any of `tool.uv.sources`, `tool.uv.dev-dependencies`, /// `project.dependencies`, and `project.optional-dependencies`. -fn store_credentials_from_target(target: InstallTarget<'_>) { +fn store_credentials_from_target(target: InstallTarget<'_>, client_builder: &BaseClientBuilder) { // Iterate over any indexes in the target. for index in target.indexes() { if let Some(credentials) = index.credentials() { if let Some(root_url) = index.root_url() { - uv_auth::store_credentials(&root_url, credentials.clone()); + client_builder.store_credentials(&root_url, credentials.clone()); } - uv_auth::store_credentials(index.raw_url(), credentials); + client_builder.store_credentials(index.raw_url(), credentials); } } @@ -946,7 +960,7 @@ fn store_credentials_from_target(target: InstallTarget<'_>) { uv_git::store_credentials_from_url(git); } Source::Url { url, .. } => { - uv_auth::store_credentials_from_url(url); + client_builder.store_credentials_from_url(url); } _ => {} } @@ -962,7 +976,7 @@ fn store_credentials_from_target(target: InstallTarget<'_>) { uv_git::store_credentials_from_url(url.repository()); } ParsedUrl::Archive(ParsedArchiveUrl { url, .. }) => { - uv_auth::store_credentials_from_url(url); + client_builder.store_credentials_from_url(url); } _ => {} }