From 05fa19c440ff4909d4b5ba771d87d912d5f91a07 Mon Sep 17 00:00:00 2001 From: konsti Date: Wed, 3 Dec 2025 14:51:25 +0100 Subject: [PATCH] Use explicit credentials cache instead of global static (#16768) Fixes https://github.com/astral-sh/uv/issues/16447 Passing this around explicitly uncovers some behaviors where we pass e.g. the credentials store to reading the lockfile. The changes in this PR should preserve the existing behavior for now, they only make the locations we read from more explicit. Labeling this PR as "Enhancement" instead of "Internal" in case this changes behavior when it shouldn't have. --- 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 028184222..076c41d7e 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 cf08a5e05..5dc9b552f 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 e1fe92f17..e21d6c135 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 dde997b34..a1fc2507f 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 b6af465a7..3416d4b40 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 c08125766..587bca9bb 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 50151487a..33cd9e0d6 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 79b6025f2..0c392a4c2 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 f148e6dd2..afcf38bd1 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 a7797d3b2..2d8603799 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 ef2227df6..805c59b26 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 723ca4f96..fa21aa041 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 d12e0651d..058cf68db 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 63e43e6a3..8133c925d 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 3557de15e..938791f8c 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 f9482d977..2cbf4c6d6 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 e2ca8b46e..a84eac7b8 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 caafc506f..4d4e01b0e 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 f4cdbb16c..4e841c87f 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 11ccca2c2..01db57db0 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 4ba0bdd94..96e3f4a82 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 e90b05ca3..12843bb34 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 b5fdc99a9..12d0abd9c 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 9f61445de..2571fe412 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 16053de23..dc8b201d0 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 6ee926daf..2542965e8 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); } _ => {} }