diff --git a/Cargo.lock b/Cargo.lock index a9300c235cfab..2e7e1858b5ccb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -490,6 +490,12 @@ version = "0.22.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" +[[package]] +name = "base64ct" +version = "1.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d809780667f4410e7c41b07f52439b94d2bdf8528eeedc287fa38d3b7f95d82" + [[package]] name = "bisection" version = "0.1.0" @@ -1199,6 +1205,17 @@ version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "092966b41edc516079bdf31ec78a2e0588d1d0c08f78b91d8307215928642b2b" +[[package]] +name = "der" +version = "0.7.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb" +dependencies = [ + "const-oid", + "pem-rfc7468", + "zeroize", +] + [[package]] name = "deranged" version = "0.5.5" @@ -2413,6 +2430,21 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonwebtoken" +version = "9.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a87cc7a48537badeae96744432de36f4be2b4a34a05a5ef32e9dd8a1c169dde" +dependencies = [ + "base64 0.22.1", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "junction" version = "1.2.0" @@ -2446,6 +2478,9 @@ name = "lazy_static" version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" +dependencies = [ + "spin", +] [[package]] name = "libc" @@ -2453,6 +2488,12 @@ version = "0.2.178" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37c93d8daa9d8a012fd8ab92f088405fb202ea0b6ab73ee2482ae66af4f42091" +[[package]] +name = "libm" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f9fbbcab51052fe104eb5e5d351cf728d30a5be1fe14d9be8a3b097481fb97de" + [[package]] name = "libmimalloc-sys" version = "0.1.44" @@ -2774,6 +2815,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "num-bigint-dig" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7" +dependencies = [ + "lazy_static", + "libm", + "num-integer", + "num-iter", + "num-traits", + "rand 0.8.5", + "smallvec", + "zeroize", +] + [[package]] name = "num-complex" version = "0.4.6" @@ -2827,6 +2884,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" dependencies = [ "autocfg", + "libm", ] [[package]] @@ -2995,6 +3053,16 @@ version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest", + "hmac", +] + [[package]] name = "pem" version = "3.0.6" @@ -3005,6 +3073,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "pem-rfc7468" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412" +dependencies = [ + "base64ct", +] + [[package]] name = "percent-encoding" version = "2.3.2" @@ -3105,6 +3182,44 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs1" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f" +dependencies = [ + "der", + "pkcs8", + "spki", +] + +[[package]] +name = "pkcs5" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e847e2c91a18bfa887dd028ec33f2fe6f25db77db3619024764914affe8b69a6" +dependencies = [ + "aes", + "cbc", + "der", + "pbkdf2", + "scrypt", + "sha2", + "spki", +] + +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "pkcs5", + "rand_core 0.6.4", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.32" @@ -3593,6 +3708,7 @@ dependencies = [ "reqsign-command-execute-tokio", "reqsign-core", "reqsign-file-read-tokio", + "reqsign-google", "reqsign-http-send-reqwest", ] @@ -3663,6 +3779,26 @@ dependencies = [ "tokio", ] +[[package]] +name = "reqsign-google" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16e1942acf06b8638f16775e92e4ca9cca239519ea6f4e650d5924d1f0dc37d1" +dependencies = [ + "async-trait", + "http", + "jsonwebtoken", + "log", + "percent-encoding", + "rand 0.8.5", + "reqsign-core", + "reqwest", + "rsa", + "serde", + "serde_json", + "sha2", +] + [[package]] name = "reqsign-http-send-reqwest" version = "2.0.1" @@ -3858,6 +3994,27 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6c20b6793b5c2fa6553b250154b78d6d0db37e72700ae35fad9387a46f487c97" +[[package]] +name = "rsa" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "40a0376c50d0358279d9d643e4bf7b7be212f1f4ff1da9070a7b54d22ef75c88" +dependencies = [ + "const-oid", + "digest", + "num-bigint-dig", + "num-integer", + "num-traits", + "pkcs1", + "pkcs8", + "rand_core 0.6.4", + "sha2", + "signature", + "spki", + "subtle", + "zeroize", +] + [[package]] name = "rust-ini" version = "0.21.3" @@ -3978,6 +4135,15 @@ version = "1.0.20" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" +[[package]] +name = "salsa20" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97a22f5af31f73a954c10289c93e8a50cc23d971e80ee446f1f6f7137a088213" +dependencies = [ + "cipher", +] + [[package]] name = "same-file" version = "1.0.6" @@ -4048,6 +4214,17 @@ dependencies = [ "syn", ] +[[package]] +name = "scrypt" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0516a385866c09368f0b5bcd1caff3366aace790fcd46e2bb032697bb172fd1f" +dependencies = [ + "pbkdf2", + "salsa20", + "sha2", +] + [[package]] name = "seahash" version = "4.1.0" @@ -4296,6 +4473,16 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" +dependencies = [ + "digest", + "rand_core 0.6.4", +] + [[package]] name = "simd-adler32" version = "0.3.7" @@ -4314,6 +4501,18 @@ version = "2.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bbbb5d9659141646ae647b42fe094daf6c6192d1620870b449d9557f748b2daa" +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.17", + "time", +] + [[package]] name = "simplecss" version = "0.2.2" @@ -4385,6 +4584,22 @@ dependencies = [ "smallvec", ] +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "spki" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "stable_deref_trait" version = "1.2.0" diff --git a/Cargo.toml b/Cargo.toml index 9e5b5c1d71990..a8e96b278e439 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -150,7 +150,7 @@ ref-cast = { version = "1.0.24" } reflink-copy = { version = "0.1.19" } regex = { version = "1.10.6" } regex-automata = { version = "0.4.8", default-features = false, features = ["dfa-build", "dfa-search", "perf", "std", "syntax"] } -reqsign = { version = "0.18.0", features = ["aws", "default-context"], default-features = false } +reqsign = { version = "0.18.1", features = ["aws", "google", "default-context"], default-features = false } reqwest = { version = "0.12.22", default-features = false, features = ["json", "gzip", "deflate", "zstd", "stream", "system-proxy", "rustls-tls", "rustls-tls-native-roots", "socks", "multipart", "http2", "blocking"] } reqwest-middleware = { version = "0.4.2", package = "astral-reqwest-middleware", features = ["multipart"] } reqwest-retry = { version = "0.8.0", package = "astral-reqwest-retry" } diff --git a/crates/uv-auth/src/credentials.rs b/crates/uv-auth/src/credentials.rs index 2839752e3efc2..846b99bd95f2b 100644 --- a/crates/uv-auth/src/credentials.rs +++ b/crates/uv-auth/src/credentials.rs @@ -9,7 +9,8 @@ use base64::read::DecoderReader; use base64::write::EncoderWriter; use http::Uri; use netrc::Netrc; -use reqsign::aws::DefaultSigner; +use reqsign::aws::DefaultSigner as AwsDefaultSigner; +use reqsign::google::DefaultSigner as GcsDefaultSigner; use reqwest::Request; use reqwest::header::HeaderValue; use serde::{Deserialize, Serialize}; @@ -388,14 +389,18 @@ pub(crate) enum Authentication { Credentials(Credentials), /// AWS Signature Version 4 signing. - Signer(DefaultSigner), + AwsSigner(AwsDefaultSigner), + + /// Google Cloud signing. + GcsSigner(GcsDefaultSigner), } impl PartialEq for Authentication { fn eq(&self, other: &Self) -> bool { match (self, other) { (Self::Credentials(a), Self::Credentials(b)) => a == b, - (Self::Signer(..), Self::Signer(..)) => true, + (Self::AwsSigner(..), Self::AwsSigner(..)) => true, + (Self::GcsSigner(..), Self::GcsSigner(..)) => true, _ => false, } } @@ -409,9 +414,15 @@ impl From for Authentication { } } -impl From for Authentication { - fn from(signer: DefaultSigner) -> Self { - Self::Signer(signer) +impl From for Authentication { + fn from(signer: AwsDefaultSigner) -> Self { + Self::AwsSigner(signer) + } +} + +impl From for Authentication { + fn from(signer: GcsDefaultSigner) -> Self { + Self::GcsSigner(signer) } } @@ -420,7 +431,7 @@ impl Authentication { pub(crate) fn password(&self) -> Option<&str> { match self { Self::Credentials(credentials) => credentials.password(), - Self::Signer(..) => None, + Self::AwsSigner(..) | Self::GcsSigner(..) => None, } } @@ -428,7 +439,7 @@ impl Authentication { pub(crate) fn username(&self) -> Option<&str> { match self { Self::Credentials(credentials) => credentials.username(), - Self::Signer(..) => None, + Self::AwsSigner(..) | Self::GcsSigner(..) => None, } } @@ -436,7 +447,7 @@ impl Authentication { pub(crate) fn as_username(&self) -> Cow<'_, Username> { match self { Self::Credentials(credentials) => credentials.as_username(), - Self::Signer(..) => Cow::Owned(Username::none()), + Self::AwsSigner(..) | Self::GcsSigner(..) => Cow::Owned(Username::none()), } } @@ -444,7 +455,7 @@ impl Authentication { pub(crate) fn to_username(&self) -> Username { match self { Self::Credentials(credentials) => credentials.to_username(), - Self::Signer(..) => Username::none(), + Self::AwsSigner(..) | Self::GcsSigner(..) => Username::none(), } } @@ -452,7 +463,7 @@ impl Authentication { pub(crate) fn is_authenticated(&self) -> bool { match self { Self::Credentials(credentials) => credentials.is_authenticated(), - Self::Signer(..) => true, + Self::AwsSigner(..) | Self::GcsSigner(..) => true, } } @@ -460,7 +471,7 @@ impl Authentication { pub(crate) fn is_empty(&self) -> bool { match self { Self::Credentials(credentials) => credentials.is_empty(), - Self::Signer(..) => false, + Self::AwsSigner(..) | Self::GcsSigner(..) => false, } } @@ -471,7 +482,7 @@ impl Authentication { pub(crate) async fn authenticate(&self, mut request: Request) -> Request { match self { Self::Credentials(credentials) => credentials.authenticate(request), - Self::Signer(signer) => { + Self::AwsSigner(signer) => { // Build an `http::Request` from the `reqwest::Request`. // SAFETY: If we have a valid `reqwest::Request`, we expect (e.g.) the URL to be valid. let uri = Uri::from_str(request.url().as_str()).unwrap(); @@ -492,6 +503,34 @@ impl Authentication { // Copy over the signed headers. request.headers_mut().extend(parts.headers); + // Copy over the signed path and query, if any. + if let Some(path_and_query) = parts.uri.path_and_query() { + request.url_mut().set_path(path_and_query.path()); + request.url_mut().set_query(path_and_query.query()); + } + request + } + Self::GcsSigner(signer) => { + // Build an `http::Request` from the `reqwest::Request`. + // SAFETY: If we have a valid `reqwest::Request`, we expect (e.g.) the URL to be valid. + let uri = Uri::from_str(request.url().as_str()).unwrap(); + let mut http_req = http::Request::builder() + .method(request.method().clone()) + .uri(uri) + .body(()) + .unwrap(); + *http_req.headers_mut() = request.headers().clone(); + + // Sign the parts. + let (mut parts, ()) = http_req.into_parts(); + signer + .sign(&mut parts, None) + .await + .expect("GCS signing should succeed"); + + // Copy over the signed headers. + request.headers_mut().extend(parts.headers); + // Copy over the signed path and query, if any. if let Some(path_and_query) = parts.uri.path_and_query() { request.url_mut().set_path(path_and_query.path()); diff --git a/crates/uv-auth/src/middleware.rs b/crates/uv-auth/src/middleware.rs index 5fd2e3b288db0..005c4e00914ce 100644 --- a/crates/uv-auth/src/middleware.rs +++ b/crates/uv-auth/src/middleware.rs @@ -14,7 +14,7 @@ use uv_static::EnvVars; use uv_warnings::owo_colors::OwoColorize; use crate::credentials::Authentication; -use crate::providers::{HuggingFaceProvider, S3EndpointProvider}; +use crate::providers::{GcsEndpointProvider, HuggingFaceProvider, S3EndpointProvider}; use crate::pyx::{DEFAULT_TOLERANCE_SECS, PyxTokenStore}; use crate::{ AccessToken, CredentialsCache, KeyringProvider, @@ -138,6 +138,15 @@ enum S3CredentialState { Initialized(Option>), } +#[derive(Clone)] +enum GcsCredentialState { + /// The GCS credential state has not yet been initialized. + Uninitialized, + /// The GCS credential state has been initialized, with either a signer or `None` if + /// no GCS endpoint is configured. + Initialized(Option>), +} + /// A middleware that adds basic authentication to requests. /// /// Uses a cache to propagate credentials from previously seen requests and @@ -161,6 +170,8 @@ pub struct AuthMiddleware { pyx_token_state: Mutex, /// Cached S3 credentials to avoid running the credential helper multiple times. s3_credential_state: Mutex, + /// Cached GCS credentials to avoid running the credential helper multiple times. + gcs_credential_state: Mutex, preview: Preview, } @@ -184,6 +195,7 @@ impl AuthMiddleware { pyx_token_store: None, pyx_token_state: Mutex::new(TokenState::Uninitialized), s3_credential_state: Mutex::new(S3CredentialState::Uninitialized), + gcs_credential_state: Mutex::new(GcsCredentialState::Uninitialized), preview: Preview::default(), } } @@ -712,6 +724,28 @@ impl AuthMiddleware { } } + if GcsEndpointProvider::is_gcs_endpoint(url, self.preview) { + let mut gcs_state = self.gcs_credential_state.lock().await; + + // If the GCS credential state is uninitialized, initialize it. + let credentials = match &*gcs_state { + GcsCredentialState::Uninitialized => { + trace!("Initializing GCS credentials for {url}"); + let signer = GcsEndpointProvider::create_signer(); + let credentials = Arc::new(Authentication::from(signer)); + *gcs_state = GcsCredentialState::Initialized(Some(credentials.clone())); + Some(credentials) + } + GcsCredentialState::Initialized(credentials) => credentials.clone(), + }; + + if let Some(credentials) = credentials { + debug!("Found GCS credentials for {url}"); + self.cache().fetches.done(key, Some(credentials.clone())); + return Some(credentials); + } + } + // If this is a known URL, authenticate it via the token store. if let Some(base_client) = self.base_client.as_ref() { if let Some(token_store) = self.pyx_token_store.as_ref() { diff --git a/crates/uv-auth/src/providers.rs b/crates/uv-auth/src/providers.rs index d0c85c6609e44..cf5c7ce4ff534 100644 --- a/crates/uv-auth/src/providers.rs +++ b/crates/uv-auth/src/providers.rs @@ -1,7 +1,8 @@ use std::borrow::Cow; use std::sync::LazyLock; -use reqsign::aws::DefaultSigner; +use reqsign::aws::DefaultSigner as AwsDefaultSigner; +use reqsign::google::DefaultSigner as GcsDefaultSigner; use tracing::debug; use url::Url; @@ -89,7 +90,7 @@ impl S3EndpointProvider { /// /// This is potentially expensive as it may invoke credential helpers, so the result /// should be cached. - pub(crate) fn create_signer() -> DefaultSigner { + pub(crate) fn create_signer() -> AwsDefaultSigner { // TODO(charlie): Can `reqsign` infer the region for us? Profiles, for example, // often have a region set already. let region = std::env::var(EnvVars::AWS_REGION) @@ -102,3 +103,43 @@ impl S3EndpointProvider { reqsign::aws::default_signer("s3", ®ion) } } + +/// The [`Url`] for the GCS endpoint, if set. +static GCS_ENDPOINT_REALM: LazyLock> = LazyLock::new(|| { + let gcs_endpoint_url = std::env::var(EnvVars::UV_GCS_ENDPOINT_URL).ok()?; + let url = Url::parse(&gcs_endpoint_url).expect("Failed to parse GCS endpoint URL"); + Some(Realm::from(&url)) +}); + +/// A provider for authentication credentials for GCS endpoints. +#[derive(Debug, Clone, PartialEq, Eq)] +pub(crate) struct GcsEndpointProvider; + +impl GcsEndpointProvider { + /// Returns `true` if the URL matches the configured GCS endpoint. + pub(crate) fn is_gcs_endpoint(url: &Url, preview: Preview) -> bool { + if let Some(gcs_endpoint_realm) = GCS_ENDPOINT_REALM.as_ref().map(RealmRef::from) { + if !preview.is_enabled(PreviewFeatures::GCS_ENDPOINT) { + warn_user_once!( + "The `gcs-endpoint` option is experimental and may change without warning. Pass `--preview-features {}` to disable this warning.", + PreviewFeatures::GCS_ENDPOINT + ); + } + + // Treat any URL on the same domain or subdomain as available for GCS signing. + let realm = RealmRef::from(url); + if realm == gcs_endpoint_realm || realm.is_subdomain_of(gcs_endpoint_realm) { + return true; + } + } + false + } + + /// Creates a new GCS signer. + /// + /// This is potentially expensive as it may invoke credential helpers, so the result + /// should be cached. + pub(crate) fn create_signer() -> GcsDefaultSigner { + reqsign::google::default_signer("storage.googleapis.com") + } +} diff --git a/crates/uv-preview/src/lib.rs b/crates/uv-preview/src/lib.rs index 35232af859d65..e2acd6adf7282 100644 --- a/crates/uv-preview/src/lib.rs +++ b/crates/uv-preview/src/lib.rs @@ -30,6 +30,7 @@ bitflags::bitflags! { const DIRECT_PUBLISH = 1 << 18; const TARGET_WORKSPACE_DISCOVERY = 1 << 19; const METADATA_JSON = 1 << 20; + const GCS_ENDPOINT = 1 << 21; } } @@ -60,6 +61,7 @@ impl PreviewFeatures { Self::DIRECT_PUBLISH => "direct-publish", Self::TARGET_WORKSPACE_DISCOVERY => "target-workspace-discovery", Self::METADATA_JSON => "metadata-json", + Self::GCS_ENDPOINT => "gcs-endpoint", _ => panic!("`flag_as_str` can only be used for exactly one feature flag"), } } @@ -108,6 +110,7 @@ impl FromStr for PreviewFeatures { "format" => Self::FORMAT, "native-auth" => Self::NATIVE_AUTH, "s3-endpoint" => Self::S3_ENDPOINT, + "gcs-endpoint" => Self::GCS_ENDPOINT, "cache-size" => Self::CACHE_SIZE, "init-project-flag" => Self::INIT_PROJECT_FLAG, "workspace-metadata" => Self::WORKSPACE_METADATA, @@ -293,6 +296,7 @@ mod tests { ); assert_eq!(PreviewFeatures::FORMAT.flag_as_str(), "format"); assert_eq!(PreviewFeatures::S3_ENDPOINT.flag_as_str(), "s3-endpoint"); + assert_eq!(PreviewFeatures::GCS_ENDPOINT.flag_as_str(), "gcs-endpoint"); assert_eq!(PreviewFeatures::SBOM_EXPORT.flag_as_str(), "sbom-export"); assert_eq!( PreviewFeatures::DIRECT_PUBLISH.flag_as_str(), diff --git a/crates/uv-static/src/env_vars.rs b/crates/uv-static/src/env_vars.rs index 69451f38696d0..614020a7f7db3 100644 --- a/crates/uv-static/src/env_vars.rs +++ b/crates/uv-static/src/env_vars.rs @@ -1204,6 +1204,12 @@ impl EnvVars { #[attr_added_in("0.8.21")] pub const UV_S3_ENDPOINT_URL: &'static str = "UV_S3_ENDPOINT_URL"; + /// The URL to treat as a GCS-compatible storage endpoint. Requests to this endpoint + /// will be signed using Google Cloud authentication based on the `GOOGLE_APPLICATION_CREDENTIALS` + /// environment variable or Application Default Credentials. + #[attr_added_in("next release")] + pub const UV_GCS_ENDPOINT_URL: &'static str = "UV_GCS_ENDPOINT_URL"; + /// The URL of the pyx Simple API server. #[attr_added_in("0.8.15")] pub const PYX_API_URL: &'static str = "PYX_API_URL"; diff --git a/crates/uv/tests/it/show_settings.rs b/crates/uv/tests/it/show_settings.rs index 5186a20eddd92..c25fa435b3f74 100644 --- a/crates/uv/tests/it/show_settings.rs +++ b/crates/uv/tests/it/show_settings.rs @@ -7982,7 +7982,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER | DIRECT_PUBLISH | TARGET_WORKSPACE_DISCOVERY | METADATA_JSON, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER | DIRECT_PUBLISH | TARGET_WORKSPACE_DISCOVERY | METADATA_JSON | GCS_ENDPOINT, ), }, python_preference: Managed, @@ -8220,7 +8220,7 @@ fn preview_features() { show_settings: true, preview: Preview { flags: PreviewFeatures( - PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER | DIRECT_PUBLISH | TARGET_WORKSPACE_DISCOVERY | METADATA_JSON, + PYTHON_INSTALL_DEFAULT | PYTHON_UPGRADE | JSON_OUTPUT | PYLOCK | ADD_BOUNDS | PACKAGE_CONFLICTS | EXTRA_BUILD_DEPENDENCIES | DETECT_MODULE_CONFLICTS | FORMAT | NATIVE_AUTH | S3_ENDPOINT | CACHE_SIZE | INIT_PROJECT_FLAG | WORKSPACE_METADATA | WORKSPACE_DIR | WORKSPACE_LIST | SBOM_EXPORT | AUTH_HELPER | DIRECT_PUBLISH | TARGET_WORKSPACE_DISCOVERY | METADATA_JSON | GCS_ENDPOINT, ), }, python_preference: Managed,