mirror of
https://github.com/astral-sh/uv.git
synced 2025-08-04 19:08:04 +00:00
feat: add linehaul info to uv-client (#2493)
## Summary
Closes #1958
This adds linehaul metadata to uv's user-agent when pep 508 markers are
provided to the RegistryClientBuilder. Thanks to #2381, we were able to
leverage most information from markers and avoid inconsistency.
Linehaul is meant to be accompanying metadata pip sends in it's user
agent when talking to registries. You can see this output by running
something like `python -c 'from pip._internal.network.session import
user_agent; print(user_agent())'`.
In PyPI, this metadata processed by the
[linehaul-cloud-function](https://github.com/pypi/linehaul-cloud-function).
More info about linehaul can be found in #1958.
Below are some examples from pip:
* Linux GHA: `pip/24.0
{"ci":true,"cpu":"x86_64","distro":{"id":"jammy","libc":{"lib":"glibc","version":"2.35"},"name":"Ubuntu","version":"22.04"},"implementation":{"name":"CPython","version":"3.12.2"},"installer":{"name":"pip","version":"24.0"},"openssl_version":"OpenSSL
3.0.2 15 Mar
2022","python":"3.12.2","rustc_version":"1.76.0","system":{"name":"Linux","release":"6.5.0-1016-azure"}}`
* Windows GHA: `pip/24.0
{"ci":true,"cpu":"AMD64","implementation":{"name":"CPython","version":"3.12.2"},"installer":{"name":"pip","version":"24.0"},"openssl_version":"OpenSSL
3.0.13 30 Jan
2024","python":"3.12.2","rustc_version":"1.76.0","system":{"name":"Windows","release":"2022Server"}}`
* OSX GHA: `pip/24.0
{"ci":true,"cpu":"arm64","distro":{"name":"macOS","version":"14.2.1"},"implementation":{"name":"CPython","version":"3.12.2"},"installer":{"name":"pip","version":"24.0"},"openssl_version":"OpenSSL
3.0.13 30 Jan
2024","python":"3.12.2","rustc_version":"1.76.0","system":{"name":"Darwin","release":"23.2.0"}}`
Here's how uv results look like (sorry for the keys not having the same
order):
* Linux GHA: `uv/0.1.21
{"installer":{"name":"uv","version":"0.1.21"},"python":"3.12.2","implementation":{"name":"CPython","version":"3.12.2"},"distro":{"name":"Ubuntu","version":"22.04","id":"jammy","libc":null},"system":{"name":"Linux","release":"6.5.0-1016-azure"},"cpu":"x86_64","openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}`
* Windows GHA: `uv/0.1.21
{"installer":{"name":"uv","version":"0.1.21"},"python":"3.12.2","implementation":{"name":"CPython","version":"3.12.2"},"distro":null,"system":{"name":"Windows","release":"2022Server"},"cpu":"AMD64","openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}`
* OSX GHA: `uv/0.1.21
{"installer":{"name":"uv","version":"0.1.21"},"python":"3.12.2","implementation":{"name":"CPython","version":"3.12.2"},"distro":{"name":"macOS","version":"14.2.1","id":null,"libc":null},"system":{"name":"Darwin","release":"23.2.0"},"cpu":"arm64","openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}`
Distro information (such as the one pip uses `from pip._vendor import
distro` to retrieve instead of `platform` module) was not retrieved from
markers. Instead, the linux release codename/name/version uses
`sys-info` crate, adding about 50us of extra overhead on linux. The
distro osx version re-used the [mac_os version
implementation](99c992e38b/crates/platform-host/src/mac_os.rs
)
from #2381 which adds about 20us of overhead on osx. I tried to use
other crates to avoid re-introducing `mac_os.rs` but most of them didn't
yield satisfactory performance (40ms-60ms~) or had the wrong values
needed (e.g. darwin version vs osx version).
I also didn't add libc retrieval or rustc retrieval as those seem to add
substantial overhead due to querying `ldd` or `rustc`. PyPy version
detection was also not added to avoid adding extra overhead to [support
PyPy for
linehaul](https://github.com/pypa/pip/blob/24.0/src/pip/_internal/network/session.py#L123).
All other behavior was kept 1-1 to match what pip's linehaul
implementation does (as of 24.0). This also aligns with what was
discussed in #1958.
## Test Plan
Added new integration test to uv-client.
---------
Co-authored-by: konstin <konstin@mailbox.org>
This commit is contained in:
parent
a07438f52f
commit
42973cd9cb
13 changed files with 512 additions and 8 deletions
114
Cargo.lock
generated
114
Cargo.lock
generated
|
@ -885,6 +885,15 @@ version = "0.1.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "63dfa964fe2a66f3fde91fc70b267fe193d822c7e603e2a675a49a7f46ad3f49"
|
||||
|
||||
[[package]]
|
||||
name = "deranged"
|
||||
version = "0.3.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
|
||||
dependencies = [
|
||||
"powerfmt",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derivative"
|
||||
version = "2.2.0"
|
||||
|
@ -1956,6 +1965,15 @@ dependencies = [
|
|||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line-wrap"
|
||||
version = "0.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f30344350a2a51da54c1d53be93fade8a237e545dbcc4bdbe635413f2117cab9"
|
||||
dependencies = [
|
||||
"safemem",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "linked-hash-map"
|
||||
version = "0.5.6"
|
||||
|
@ -2168,6 +2186,12 @@ dependencies = [
|
|||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num-conv"
|
||||
version = "0.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
|
||||
|
||||
[[package]]
|
||||
name = "num-traits"
|
||||
version = "0.2.18"
|
||||
|
@ -2256,6 +2280,16 @@ version = "0.2.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d"
|
||||
|
||||
[[package]]
|
||||
name = "os_info"
|
||||
version = "3.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "006e42d5b888366f1880eda20371fedde764ed2213dc8496f49622fa0c99cd5e"
|
||||
dependencies = [
|
||||
"log",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "overload"
|
||||
version = "0.1.1"
|
||||
|
@ -2459,6 +2493,20 @@ dependencies = [
|
|||
"thiserror",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e5699cc8a63d1aa2b1ee8e12b9ad70ac790d65788cd36101fa37f87ea46c4cef"
|
||||
dependencies = [
|
||||
"base64 0.21.7",
|
||||
"indexmap 2.2.5",
|
||||
"line-wrap",
|
||||
"quick-xml",
|
||||
"serde",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "png"
|
||||
version = "0.17.13"
|
||||
|
@ -2487,6 +2535,12 @@ version = "1.6.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0"
|
||||
|
||||
[[package]]
|
||||
name = "powerfmt"
|
||||
version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
|
||||
|
||||
[[package]]
|
||||
name = "ppv-lite86"
|
||||
version = "0.2.17"
|
||||
|
@ -2684,6 +2738,15 @@ dependencies = [
|
|||
"toml",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quick-xml"
|
||||
version = "0.31.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.35"
|
||||
|
@ -3210,6 +3273,12 @@ version = "1.0.17"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e86697c916019a8588c99b5fac3cead74ec0b4b819707a682fd4d23fa0ce1ba1"
|
||||
|
||||
[[package]]
|
||||
name = "safemem"
|
||||
version = "0.3.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
|
@ -3547,6 +3616,16 @@ version = "0.1.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160"
|
||||
|
||||
[[package]]
|
||||
name = "sys-info"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "system-configuration"
|
||||
version = "0.5.1"
|
||||
|
@ -3726,6 +3805,37 @@ dependencies = [
|
|||
"tikv-jemalloc-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8248b6521bb14bc45b4067159b9b6ad792e2d6d754d6c41fb50e29fefe38749"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"num-conv",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
"time-macros",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "time-core"
|
||||
version = "0.1.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.17"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7ba3a3ef41e6672a2f0f001392bb5dcd3ff0a9992d618ca761a11c3121547774"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tiny-skia"
|
||||
version = "0.8.4"
|
||||
|
@ -4361,9 +4471,11 @@ dependencies = [
|
|||
"hyper 0.14.28",
|
||||
"insta",
|
||||
"install-wheel-rs",
|
||||
"os_info",
|
||||
"pep440_rs",
|
||||
"pep508_rs",
|
||||
"platform-tags",
|
||||
"plist",
|
||||
"pypi-types",
|
||||
"reqwest",
|
||||
"reqwest-middleware",
|
||||
|
@ -4376,6 +4488,7 @@ dependencies = [
|
|||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"sys-info",
|
||||
"task-local-extensions",
|
||||
"tempfile",
|
||||
"thiserror",
|
||||
|
@ -4388,6 +4501,7 @@ dependencies = [
|
|||
"uv-auth",
|
||||
"uv-cache",
|
||||
"uv-fs",
|
||||
"uv-interpreter",
|
||||
"uv-normalize",
|
||||
"uv-version",
|
||||
"uv-warnings",
|
||||
|
|
|
@ -68,6 +68,7 @@ owo-colors = { version = "4.0.0" }
|
|||
pathdiff = { version = "0.2.1" }
|
||||
petgraph = { version = "0.6.4" }
|
||||
platform-info = { version = "2.0.2" }
|
||||
plist = { version = "1.6.0" }
|
||||
pubgrub = { git = "https://github.com/astral-sh/pubgrub", rev = "addbaf184891d66a2dfd93d241a66d13bfe5de86" }
|
||||
pyo3 = { version = "0.20.3" }
|
||||
pyo3-log = { version = "0.9.0" }
|
||||
|
@ -89,6 +90,7 @@ serde = { version = "1.0.197" }
|
|||
serde_json = { version = "1.0.114" }
|
||||
sha1 = { version = "0.10.6" }
|
||||
sha2 = { version = "0.10.8" }
|
||||
sys-info = { version = "0.9.1" }
|
||||
target-lexicon = { version = "0.12.14" }
|
||||
task-local-extensions = { version = "0.1.4" }
|
||||
tempfile = { version = "3.9.0" }
|
||||
|
|
|
@ -14,6 +14,7 @@ platform-tags = { path = "../platform-tags" }
|
|||
uv-auth = { path = "../uv-auth" }
|
||||
uv-cache = { path = "../uv-cache" }
|
||||
uv-fs = { path = "../uv-fs", features = ["tokio"] }
|
||||
uv-interpreter = { path = "../uv-interpreter" }
|
||||
uv-normalize = { path = "../uv-normalize" }
|
||||
uv-version = { path = "../uv-version" }
|
||||
uv-warnings = { path = "../uv-warnings" }
|
||||
|
@ -28,6 +29,7 @@ fs-err = { workspace = true, features = ["tokio"] }
|
|||
futures = { workspace = true }
|
||||
html-escape = { workspace = true }
|
||||
http = { workspace = true }
|
||||
plist = { workspace = true }
|
||||
reqwest = { workspace = true }
|
||||
reqwest-middleware = { workspace = true }
|
||||
reqwest-retry = { workspace = true }
|
||||
|
@ -37,6 +39,7 @@ rustc-hash = { workspace = true }
|
|||
serde = { workspace = true }
|
||||
serde_json = { workspace = true }
|
||||
sha2 = { workspace = true }
|
||||
sys-info = { workspace = true }
|
||||
task-local-extensions = { workspace = true }
|
||||
tempfile = { workspace = true }
|
||||
thiserror = { workspace = true }
|
||||
|
@ -56,4 +59,5 @@ webpki-roots = { version = "0.25.4" }
|
|||
anyhow = { workspace = true }
|
||||
hyper = { version = "0.14.28", features = ["server", "http1"] }
|
||||
insta = { version = "1.36.1" }
|
||||
os_info = { version = "3.7.0", default-features = false }
|
||||
tokio = { workspace = true, features = ["fs", "macros"] }
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
use pep508_rs::MarkerEnvironment;
|
||||
use platform_tags::Platform;
|
||||
use reqwest::{Client, ClientBuilder};
|
||||
use reqwest_middleware::ClientWithMiddleware;
|
||||
use reqwest_retry::policies::ExponentialBackoff;
|
||||
|
@ -12,21 +14,24 @@ use uv_fs::Simplified;
|
|||
use uv_version::version;
|
||||
use uv_warnings::warn_user_once;
|
||||
|
||||
use crate::linehaul::LineHaul;
|
||||
use crate::middleware::OfflineMiddleware;
|
||||
use crate::tls::Roots;
|
||||
use crate::{tls, Connectivity};
|
||||
|
||||
/// A builder for an [`RegistryClient`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BaseClientBuilder {
|
||||
pub struct BaseClientBuilder<'a> {
|
||||
keyring_provider: KeyringProvider,
|
||||
native_tls: bool,
|
||||
retries: u32,
|
||||
connectivity: Connectivity,
|
||||
client: Option<Client>,
|
||||
markers: Option<&'a MarkerEnvironment>,
|
||||
platform: Option<&'a Platform>,
|
||||
}
|
||||
|
||||
impl BaseClientBuilder {
|
||||
impl BaseClientBuilder<'_> {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
keyring_provider: KeyringProvider::default(),
|
||||
|
@ -34,11 +39,13 @@ impl BaseClientBuilder {
|
|||
connectivity: Connectivity::Online,
|
||||
retries: 3,
|
||||
client: None,
|
||||
markers: None,
|
||||
platform: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl BaseClientBuilder {
|
||||
impl<'a> BaseClientBuilder<'a> {
|
||||
#[must_use]
|
||||
pub fn keyring_provider(mut self, keyring_provider: KeyringProvider) -> Self {
|
||||
self.keyring_provider = keyring_provider;
|
||||
|
@ -69,9 +76,29 @@ impl BaseClientBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn markers(mut self, markers: &'a MarkerEnvironment) -> Self {
|
||||
self.markers = Some(markers);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn platform(mut self, platform: &'a Platform) -> Self {
|
||||
self.platform = Some(platform);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> BaseClient {
|
||||
// Create user agent.
|
||||
let user_agent_string = format!("uv/{}", version());
|
||||
let mut user_agent_string = format!("uv/{}", version());
|
||||
|
||||
// Add linehaul metadata.
|
||||
if let Some(markers) = self.markers {
|
||||
let linehaul = LineHaul::new(markers, self.platform);
|
||||
if let Ok(output) = serde_json::to_string(&linehaul) {
|
||||
user_agent_string += &format!(" {}", output);
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
|
|
|
@ -2,6 +2,7 @@ pub use base_client::BaseClient;
|
|||
pub use cached_client::{CacheControl, CachedClient, CachedClientError, DataWithCachePolicy};
|
||||
pub use error::{BetterReqwestError, Error, ErrorKind};
|
||||
pub use flat_index::{FlatDistributions, FlatIndex, FlatIndexClient, FlatIndexError};
|
||||
pub use linehaul::LineHaul;
|
||||
pub use registry_client::{
|
||||
Connectivity, RegistryClient, RegistryClientBuilder, SimpleMetadata, SimpleMetadatum,
|
||||
VersionFiles,
|
||||
|
@ -14,6 +15,8 @@ mod error;
|
|||
mod flat_index;
|
||||
mod html;
|
||||
mod httpcache;
|
||||
mod linehaul;
|
||||
mod mac_version;
|
||||
mod middleware;
|
||||
mod registry_client;
|
||||
mod remote_metadata;
|
||||
|
|
134
crates/uv-client/src/linehaul.rs
Normal file
134
crates/uv-client/src/linehaul.rs
Normal file
|
@ -0,0 +1,134 @@
|
|||
use std::env;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tracing::instrument;
|
||||
|
||||
use pep508_rs::MarkerEnvironment;
|
||||
use platform_tags::{Os, Platform};
|
||||
use uv_version::version;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Installer {
|
||||
pub name: Option<String>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Implementation {
|
||||
pub name: Option<String>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Libc {
|
||||
pub lib: Option<String>,
|
||||
pub version: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct Distro {
|
||||
pub name: Option<String>,
|
||||
pub version: Option<String>,
|
||||
pub id: Option<String>,
|
||||
pub libc: Option<Libc>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct System {
|
||||
pub name: Option<String>,
|
||||
pub release: Option<String>,
|
||||
}
|
||||
|
||||
/// Linehaul structs were derived from
|
||||
/// <https://github.com/pypi/linehaul-cloud-function/blob/1.0.1/linehaul/ua/datastructures.py>.
|
||||
/// For the sake of parity, the nullability of all the values was kept intact.
|
||||
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
|
||||
pub struct LineHaul {
|
||||
pub installer: Option<Installer>,
|
||||
pub python: Option<String>,
|
||||
pub implementation: Option<Implementation>,
|
||||
pub distro: Option<Distro>,
|
||||
pub system: Option<System>,
|
||||
pub cpu: Option<String>,
|
||||
pub openssl_version: Option<String>,
|
||||
pub setuptools_version: Option<String>,
|
||||
pub rustc_version: Option<String>,
|
||||
pub ci: Option<bool>,
|
||||
}
|
||||
|
||||
/// Implements Linehaul information format as defined by
|
||||
/// <https://github.com/pypa/pip/blob/24.0/src/pip/_internal/network/session.py#L109>.
|
||||
/// This metadata is added to the user agent to enrich PyPI statistics.
|
||||
impl LineHaul {
|
||||
/// Initializes Linehaul information based on PEP 508 markers.
|
||||
#[instrument(name = "linehaul", skip_all)]
|
||||
pub fn new(markers: &MarkerEnvironment, platform: Option<&Platform>) -> Self {
|
||||
// https://github.com/pypa/pip/blob/24.0/src/pip/_internal/network/session.py#L87
|
||||
let looks_like_ci = ["BUILD_BUILDID", "BUILD_ID", "CI", "PIP_IS_CI"]
|
||||
.iter()
|
||||
.find_map(|&var_name| env::var(var_name).ok().map(|_| true));
|
||||
|
||||
let libc = match platform.map(|platform| platform.os()) {
|
||||
Some(Os::Manylinux { major, minor }) => Some(Libc {
|
||||
lib: Some("glibc".to_string()),
|
||||
version: Some(format!("{major}.{minor}")),
|
||||
}),
|
||||
Some(Os::Musllinux { major, minor }) => Some(Libc {
|
||||
lib: Some("musl".to_string()),
|
||||
version: Some(format!("{major}.{minor}")),
|
||||
}),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
// Build Distro as Linehaul expects.
|
||||
let distro: Option<Distro> = if cfg!(target_os = "linux") {
|
||||
// Gather distribution info from /etc/os-release.
|
||||
sys_info::linux_os_release().ok().map(|info| Distro {
|
||||
// e.g., Jammy, Focal, etc.
|
||||
id: info.version_codename,
|
||||
// e.g., Ubuntu, Fedora, etc.
|
||||
name: info.name,
|
||||
// e.g., 22.04, etc.
|
||||
version: info.version_id,
|
||||
// e.g., glibc 2.38, musl 1.2
|
||||
libc,
|
||||
})
|
||||
} else if cfg!(target_os = "macos") {
|
||||
Some(Distro {
|
||||
// N/A
|
||||
id: None,
|
||||
// pip hardcodes distro name to macOS.
|
||||
name: Some("macOS".to_string()),
|
||||
// Same as python's platform.mac_ver[0].
|
||||
version: crate::mac_version::get_mac_os_version().ok(),
|
||||
// N/A
|
||||
libc: None,
|
||||
})
|
||||
} else {
|
||||
// Always empty on Windows.
|
||||
None
|
||||
};
|
||||
|
||||
Self {
|
||||
installer: Option::from(Installer {
|
||||
name: Some("uv".to_string()),
|
||||
version: Some(version().to_string()),
|
||||
}),
|
||||
python: Some(markers.python_full_version.version.to_string()),
|
||||
implementation: Option::from(Implementation {
|
||||
name: Some(markers.platform_python_implementation.to_string()),
|
||||
version: Some(markers.python_full_version.version.to_string()),
|
||||
}),
|
||||
distro,
|
||||
system: Option::from(System {
|
||||
name: Some(markers.platform_system.to_string()),
|
||||
release: Some(markers.platform_release.to_string()),
|
||||
}),
|
||||
cpu: Some(markers.platform_machine.to_string()),
|
||||
openssl_version: None, // Should probably always be None in uv.
|
||||
setuptools_version: None, // Should probably always be None in uv.
|
||||
rustc_version: None, // Calling rustc --version is likely too slow.
|
||||
ci: looks_like_ci,
|
||||
}
|
||||
}
|
||||
}
|
36
crates/uv-client/src/mac_version.rs
Normal file
36
crates/uv-client/src/mac_version.rs
Normal file
|
@ -0,0 +1,36 @@
|
|||
use platform_tags::PlatformError;
|
||||
use serde::Deserialize;
|
||||
|
||||
/// Get the macOS version from the SystemVersion.plist file.
|
||||
pub(crate) fn get_mac_os_version() -> Result<String, PlatformError> {
|
||||
// This is actually what python does
|
||||
// https://github.com/python/cpython/blob/cb2b3c8d3566ae46b3b8d0718019e1c98484589e/Lib/platform.py#L409-L428
|
||||
#[derive(Deserialize)]
|
||||
#[serde(rename_all = "PascalCase")]
|
||||
struct SystemVersion {
|
||||
product_version: String,
|
||||
}
|
||||
let system_version: SystemVersion =
|
||||
plist::from_file("/System/Library/CoreServices/SystemVersion.plist")
|
||||
.map_err(|err| PlatformError::OsVersionDetectionError(err.to_string()))?;
|
||||
|
||||
let invalid_mac_os_version = || {
|
||||
PlatformError::OsVersionDetectionError(format!(
|
||||
"Invalid macOS version {}",
|
||||
system_version.product_version
|
||||
))
|
||||
};
|
||||
match system_version
|
||||
.product_version
|
||||
.split('.')
|
||||
.collect::<Vec<&str>>()
|
||||
.as_slice()
|
||||
{
|
||||
[major, minor] | [major, minor, _] => {
|
||||
let _major = major.parse::<u16>().map_err(|_| invalid_mac_os_version())?;
|
||||
let _minor = minor.parse::<u16>().map_err(|_| invalid_mac_os_version())?;
|
||||
Ok(system_version.product_version)
|
||||
}
|
||||
_ => Err(invalid_mac_os_version()),
|
||||
}
|
||||
}
|
|
@ -17,6 +17,8 @@ use distribution_filename::{DistFilename, SourceDistFilename, WheelFilename};
|
|||
use distribution_types::{BuiltDist, File, FileLocation, IndexUrl, IndexUrls, Name};
|
||||
use install_wheel_rs::metadata::{find_archive_dist_info, is_metadata_entry};
|
||||
use pep440_rs::Version;
|
||||
use pep508_rs::MarkerEnvironment;
|
||||
use platform_tags::Platform;
|
||||
use pypi_types::{Metadata23, SimpleJson};
|
||||
use uv_auth::KeyringProvider;
|
||||
use uv_cache::{Cache, CacheBucket, WheelCache};
|
||||
|
@ -31,7 +33,7 @@ use crate::{CachedClient, CachedClientError, Error, ErrorKind};
|
|||
|
||||
/// A builder for an [`RegistryClient`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RegistryClientBuilder {
|
||||
pub struct RegistryClientBuilder<'a> {
|
||||
index_urls: IndexUrls,
|
||||
keyring_provider: KeyringProvider,
|
||||
native_tls: bool,
|
||||
|
@ -39,9 +41,11 @@ pub struct RegistryClientBuilder {
|
|||
connectivity: Connectivity,
|
||||
cache: Cache,
|
||||
client: Option<Client>,
|
||||
markers: Option<&'a MarkerEnvironment>,
|
||||
platform: Option<&'a Platform>,
|
||||
}
|
||||
|
||||
impl RegistryClientBuilder {
|
||||
impl RegistryClientBuilder<'_> {
|
||||
pub fn new(cache: Cache) -> Self {
|
||||
Self {
|
||||
index_urls: IndexUrls::default(),
|
||||
|
@ -51,11 +55,13 @@ impl RegistryClientBuilder {
|
|||
connectivity: Connectivity::Online,
|
||||
retries: 3,
|
||||
client: None,
|
||||
markers: None,
|
||||
platform: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RegistryClientBuilder {
|
||||
impl<'a> RegistryClientBuilder<'a> {
|
||||
#[must_use]
|
||||
pub fn index_urls(mut self, index_urls: IndexUrls) -> Self {
|
||||
self.index_urls = index_urls;
|
||||
|
@ -98,6 +104,18 @@ impl RegistryClientBuilder {
|
|||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn markers(mut self, markers: &'a MarkerEnvironment) -> Self {
|
||||
self.markers = Some(markers);
|
||||
self
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
pub fn platform(mut self, platform: &'a Platform) -> Self {
|
||||
self.platform = Some(platform);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> RegistryClient {
|
||||
// Build a base client
|
||||
let mut builder = BaseClientBuilder::new();
|
||||
|
@ -106,6 +124,14 @@ impl RegistryClientBuilder {
|
|||
builder = builder.client(client)
|
||||
}
|
||||
|
||||
if let Some(markers) = self.markers {
|
||||
builder = builder.markers(markers)
|
||||
}
|
||||
|
||||
if let Some(platform) = self.platform {
|
||||
builder = builder.platform(platform)
|
||||
}
|
||||
|
||||
let client = builder
|
||||
.retries(self.retries)
|
||||
.connectivity(self.connectivity)
|
||||
|
|
|
@ -4,9 +4,11 @@ use hyper::header::USER_AGENT;
|
|||
use hyper::server::conn::Http;
|
||||
use hyper::service::service_fn;
|
||||
use hyper::{Body, Request, Response};
|
||||
use pep508_rs::{MarkerEnvironment, StringVersion};
|
||||
use platform_tags::{Arch, Os, Platform};
|
||||
use tokio::net::TcpListener;
|
||||
|
||||
use uv_cache::Cache;
|
||||
use uv_client::LineHaul;
|
||||
use uv_client::RegistryClientBuilder;
|
||||
use uv_version::version;
|
||||
|
||||
|
@ -60,3 +62,154 @@ async fn test_user_agent_has_version() -> Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_user_agent_has_linehaul() -> 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<Body>| {
|
||||
// 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");
|
||||
});
|
||||
|
||||
// Add some representative markers for an Ubuntu CI runner
|
||||
let markers = MarkerEnvironment {
|
||||
implementation_name: "cpython".to_string(),
|
||||
implementation_version: StringVersion {
|
||||
string: "3.12.2".to_string(),
|
||||
version: "3.12.2".parse()?,
|
||||
},
|
||||
os_name: "posix".to_string(),
|
||||
platform_machine: "x86_64".to_string(),
|
||||
platform_python_implementation: "CPython".to_string(),
|
||||
platform_release: "6.5.0-1016-azure".to_string(),
|
||||
platform_system: "Linux".to_string(),
|
||||
platform_version: "#16~22.04.1-Ubuntu SMP Fri Feb 16 15:42:02 UTC 2024".to_string(),
|
||||
python_full_version: StringVersion {
|
||||
string: "3.12.2".to_string(),
|
||||
version: "3.12.2".parse()?,
|
||||
},
|
||||
python_version: StringVersion {
|
||||
string: "3.12".to_string(),
|
||||
version: "3.12".parse()?,
|
||||
},
|
||||
sys_platform: "linux".to_string(),
|
||||
};
|
||||
// Linux only
|
||||
let platform = Platform::new(
|
||||
Os::Manylinux {
|
||||
major: 2,
|
||||
minor: 38,
|
||||
},
|
||||
Arch::X86_64,
|
||||
);
|
||||
|
||||
// Initialize uv-client
|
||||
let cache = Cache::temp()?;
|
||||
let mut builder = RegistryClientBuilder::new(cache).markers(&markers);
|
||||
|
||||
if cfg!(target_os = "linux") {
|
||||
builder = builder.platform(&platform);
|
||||
}
|
||||
let client = builder.build();
|
||||
|
||||
// Send request to our dummy server
|
||||
let res = client
|
||||
.uncached_client()
|
||||
.get(format!("http://{addr}"))
|
||||
.send()
|
||||
.await?;
|
||||
|
||||
// Check the HTTP status
|
||||
assert!(res.status().is_success());
|
||||
|
||||
// Check User Agent
|
||||
let body = res.text().await?;
|
||||
|
||||
// Unpack User-Agent with linehaul
|
||||
let (uv_version, uv_linehaul) = body
|
||||
.split_once(' ')
|
||||
.expect("Failed to split User-Agent header.");
|
||||
|
||||
// Deserializing Linehaul
|
||||
let linehaul: LineHaul = serde_json::from_str(uv_linehaul)?;
|
||||
|
||||
// Assert uv version
|
||||
assert_eq!(uv_version, format!("uv/{}", version()));
|
||||
|
||||
// Assert linehaul
|
||||
let installer_info = linehaul.installer.unwrap();
|
||||
let system_info = linehaul.system.unwrap();
|
||||
let impl_info = linehaul.implementation.unwrap();
|
||||
|
||||
assert_eq!(installer_info.name.unwrap(), "uv".to_string());
|
||||
assert_eq!(installer_info.version.unwrap(), version());
|
||||
|
||||
assert_eq!(system_info.name.unwrap(), markers.platform_system);
|
||||
assert_eq!(system_info.release.unwrap(), markers.platform_release);
|
||||
|
||||
assert_eq!(
|
||||
impl_info.name.unwrap(),
|
||||
markers.platform_python_implementation
|
||||
);
|
||||
assert_eq!(
|
||||
impl_info.version.unwrap(),
|
||||
markers.python_full_version.version.to_string()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
linehaul.python.unwrap(),
|
||||
markers.python_full_version.version.to_string()
|
||||
);
|
||||
assert_eq!(linehaul.cpu.unwrap(), markers.platform_machine);
|
||||
|
||||
assert_eq!(linehaul.openssl_version, None);
|
||||
assert_eq!(linehaul.setuptools_version, None);
|
||||
assert_eq!(linehaul.rustc_version, None);
|
||||
|
||||
#[cfg(windows)]
|
||||
assert_eq!(linehaul.distro, None);
|
||||
|
||||
// Using os_info as to confirm our values are as expected in both Linux and OSX.
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let info = os_info::get();
|
||||
let distro_info = linehaul.distro.unwrap();
|
||||
assert_eq!(distro_info.id.unwrap(), info.codename().unwrap());
|
||||
assert_eq!(distro_info.name.unwrap(), info.os_type().to_string());
|
||||
assert_eq!(distro_info.version.unwrap(), info.version().to_string());
|
||||
assert!(distro_info.libc.is_some());
|
||||
}
|
||||
|
||||
// Using os_info as sys-info yields Darwin version, and not mac release version.
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
let info = os_info::get();
|
||||
let distro_info = linehaul.distro.unwrap();
|
||||
assert_eq!(distro_info.id, None);
|
||||
assert_eq!(distro_info.name.unwrap(), "macOS");
|
||||
assert_eq!(distro_info.version.unwrap(), info.version().to_string());
|
||||
assert_eq!(distro_info.libc, None);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -198,6 +198,8 @@ pub(crate) async fn pip_compile(
|
|||
.connectivity(connectivity)
|
||||
.index_urls(index_locations.index_urls())
|
||||
.keyring_provider(keyring_provider)
|
||||
.markers(&markers)
|
||||
.platform(interpreter.platform())
|
||||
.build();
|
||||
|
||||
// Resolve the flat indexes from `--find-links`.
|
||||
|
|
|
@ -192,6 +192,7 @@ pub(crate) async fn pip_install(
|
|||
.connectivity(connectivity)
|
||||
.index_urls(index_locations.index_urls())
|
||||
.keyring_provider(keyring_provider)
|
||||
.markers(markers)
|
||||
.build();
|
||||
|
||||
// Resolve the flat indexes from `--find-links`.
|
||||
|
|
|
@ -126,6 +126,7 @@ pub(crate) async fn pip_sync(
|
|||
.connectivity(connectivity)
|
||||
.index_urls(index_locations.index_urls())
|
||||
.keyring_provider(keyring_provider)
|
||||
.markers(venv.interpreter().markers())
|
||||
.build();
|
||||
|
||||
// Resolve the flat indexes from `--find-links`.
|
||||
|
|
|
@ -151,6 +151,7 @@ async fn venv_impl(
|
|||
.index_urls(index_locations.index_urls())
|
||||
.keyring_provider(keyring_provider)
|
||||
.connectivity(connectivity)
|
||||
.markers(interpreter.markers())
|
||||
.build();
|
||||
|
||||
// Resolve the flat indexes from `--find-links`.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue