diff --git a/Cargo.lock b/Cargo.lock index f55d0d2b6..a01ee772a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4166,6 +4166,7 @@ dependencies = [ "uv-normalize", "uv-resolver", "uv-traits", + "uv-version", "uv-virtualenv", "uv-warnings", "which", @@ -4247,6 +4248,7 @@ dependencies = [ "futures", "html-escape", "http", + "hyper", "insta", "install-wheel-rs", "pep440_rs", @@ -4275,6 +4277,7 @@ dependencies = [ "uv-cache", "uv-fs", "uv-normalize", + "uv-version", "uv-warnings", ] @@ -4598,6 +4601,10 @@ dependencies = [ "uv-normalize", ] +[[package]] +name = "uv-version" +version = "0.1.14" + [[package]] name = "uv-virtualenv" version = "0.0.4" diff --git a/crates/uv-client/Cargo.toml b/crates/uv-client/Cargo.toml index 8ceb69c74..3096c8477 100644 --- a/crates/uv-client/Cargo.toml +++ b/crates/uv-client/Cargo.toml @@ -15,6 +15,7 @@ uv-auth = { path = "../uv-auth" } uv-cache = { path = "../uv-cache" } uv-fs = { path = "../uv-fs", features = ["tokio"] } uv-normalize = { path = "../uv-normalize" } +uv-version = { path = "../uv-version" } uv-warnings = { path = "../uv-warnings" } pypi-types = { path = "../pypi-types" } @@ -48,5 +49,6 @@ urlencoding = { workspace = true } [dev-dependencies] anyhow = { workspace = true } +hyper = { version = "0.14.28", features = ["server", "http1"] } insta = { version = "1.35.1" } tokio = { workspace = true, features = ["fs", "macros"] } diff --git a/crates/uv-client/src/registry_client.rs b/crates/uv-client/src/registry_client.rs index 134257ccc..fe32bb54c 100644 --- a/crates/uv-client/src/registry_client.rs +++ b/crates/uv-client/src/registry_client.rs @@ -6,7 +6,6 @@ use std::str::FromStr; use async_http_range_reader::AsyncHttpRangeReader; use futures::{FutureExt, TryStreamExt}; - use http::HeaderMap; use reqwest::{Client, ClientBuilder, Response, StatusCode}; use reqwest_retry::policies::ExponentialBackoff; @@ -25,6 +24,7 @@ use pypi_types::{Metadata21, SimpleJson}; use uv_auth::safe_copy_url_auth; use uv_cache::{Cache, CacheBucket, WheelCache}; use uv_normalize::PackageName; +use uv_version::version; use uv_warnings::warn_user_once; use crate::cached_client::CacheControl; @@ -88,6 +88,9 @@ impl RegistryClientBuilder { } pub fn build(self) -> RegistryClient { + // Create user agent. + let user_agent_string = format!("uv/{}", version()); + // Timeout options, matching https://doc.rust-lang.org/nightly/cargo/reference/config.html#httptimeout // `UV_REQUEST_TIMEOUT` is provided for backwards compatibility with v0.1.6 let default_timeout = 5 * 60; @@ -108,7 +111,7 @@ impl RegistryClientBuilder { let client_raw = self.client.unwrap_or_else(|| { // Disallow any connections. let client_core = ClientBuilder::new() - .user_agent("uv") + .user_agent(user_agent_string) .pool_max_idle_per_host(20) .timeout(std::time::Duration::from_secs(timeout)); diff --git a/crates/uv-client/tests/user_agent_version.rs b/crates/uv-client/tests/user_agent_version.rs new file mode 100644 index 000000000..a366de68d --- /dev/null +++ b/crates/uv-client/tests/user_agent_version.rs @@ -0,0 +1,63 @@ +use anyhow::Result; +use futures::future; +use hyper::header::USER_AGENT; +use hyper::server::conn::Http; +use hyper::service::service_fn; +use hyper::{Body, Request, Response}; +use tokio::net::TcpListener; + +use uv_cache::Cache; +use uv_client::RegistryClientBuilder; +use uv_version::version; + +#[tokio::test] +async fn test_user_agent_has_version() -> Result<()> { + // Set up the TCP listener on a random available port + let listener = TcpListener::bind("127.0.0.1:0").await?; + let addr = listener.local_addr()?; + + // Spawn the server loop in a background task + tokio::spawn(async move { + let svc = service_fn(move |req: Request| { + // Get User Agent Header and send it back in the response + let user_agent = req + .headers() + .get(USER_AGENT) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + .unwrap_or_default(); // Empty Default + future::ok::<_, hyper::Error>(Response::new(Body::from(user_agent))) + }); + // Start Hyper Server + let (socket, _) = listener.accept().await.unwrap(); + Http::new() + .http1_keep_alive(false) + .serve_connection(socket, svc) + .with_upgrades() + .await + .expect("Server Started"); + }); + + // Initialize uv-client + let cache = Cache::temp()?; + let client = RegistryClientBuilder::new(cache).build(); + + // Send request to our dummy server + let res = client + .cached_client() + .uncached() + .get(format!("http://{addr}")) + .send() + .await?; + + // Check the HTTP status + assert!(res.status().is_success()); + + // Check User Agent + let body = res.text().await?; + + // Verify body matches regex + assert_eq!(body, format!("uv/{}", version())); + + Ok(()) +} diff --git a/crates/uv-version/Cargo.toml b/crates/uv-version/Cargo.toml new file mode 100644 index 000000000..ac892c62b --- /dev/null +++ b/crates/uv-version/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "uv-version" +version = "0.1.14" +edition = { workspace = true } +rust-version = { workspace = true } +homepage = { workspace = true } +documentation = { workspace = true } +repository = { workspace = true } +authors = { workspace = true } +license = { workspace = true } + +[lints] +workspace = true + +[dependencies] diff --git a/crates/uv-version/src/lib.rs b/crates/uv-version/src/lib.rs new file mode 100644 index 000000000..20a8940a1 --- /dev/null +++ b/crates/uv-version/src/lib.rs @@ -0,0 +1,16 @@ +/// Return the application version. +/// +/// This should be in sync with uv's version based on the Crate version. +pub fn version() -> &'static str { + env!("CARGO_PKG_VERSION") +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_get_version() { + assert_eq!(version().to_string(), env!("CARGO_PKG_VERSION").to_string()); + } +} diff --git a/crates/uv/Cargo.toml b/crates/uv/Cargo.toml index 37ba418fc..993a7e020 100644 --- a/crates/uv/Cargo.toml +++ b/crates/uv/Cargo.toml @@ -34,6 +34,7 @@ uv-interpreter = { path = "../uv-interpreter" } uv-normalize = { path = "../uv-normalize" } uv-resolver = { path = "../uv-resolver", features = ["clap"] } uv-traits = { path = "../uv-traits" } +uv-version = { path = "../uv-version" } uv-virtualenv = { path = "../uv-virtualenv" } uv-warnings = { path = "../uv-warnings" } diff --git a/pyproject.toml b/pyproject.toml index 936723abc..ab39c22a1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -64,4 +64,5 @@ changelog_contributors = false version_files = [ "README.md", "crates/uv/Cargo.toml", + "crates/uv-version/Cargo.toml", ]