Skip to content

Commit d9d0359

Browse files
authored
Make uv self update fetch the manifest from the mirror first (#18679)
1 parent 7228ad6 commit d9d0359

3 files changed

Lines changed: 148 additions & 12 deletions

File tree

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/uv-bin-install/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,3 +37,6 @@ thiserror = { workspace = true }
3737
tokio = { workspace = true }
3838
tokio-util = { workspace = true }
3939
url = { workspace = true }
40+
41+
[dev-dependencies]
42+
wiremock = { workspace = true }

crates/uv-bin-install/src/lib.rs

Lines changed: 144 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -113,12 +113,11 @@ impl Binary {
113113
.unwrap(),
114114
DisplaySafeUrl::parse(&format!("{VERSIONS_MANIFEST_URL}/{name}.ndjson")).unwrap(),
115115
],
116-
Self::Uv => {
117-
vec![
118-
DisplaySafeUrl::parse(&format!("{VERSIONS_MANIFEST_URL}/{name}.ndjson"))
119-
.unwrap(),
120-
]
121-
}
116+
Self::Uv => vec![
117+
DisplaySafeUrl::parse(&format!("{VERSIONS_MANIFEST_MIRROR}/{name}.ndjson"))
118+
.unwrap(),
119+
DisplaySafeUrl::parse(&format!("{VERSIONS_MANIFEST_URL}/{name}.ndjson")).unwrap(),
120+
],
122121
}
123122
}
124123

@@ -393,11 +392,13 @@ impl RetriableError for Error {
393392

394393
/// Returns `true` if trying an alternative URL makes sense after this error.
395394
///
396-
/// All errors arising from downloading (including streaming during extraction)
397-
/// qualify.
395+
/// Download and streaming failures qualify, as do malformed manifest responses.
398396
fn should_try_next_url(&self) -> bool {
399397
match self {
400-
Self::Download { .. } | Self::ManifestFetch { .. } => true,
398+
Self::Download { .. }
399+
| Self::ManifestFetch { .. }
400+
| Self::ManifestParse(..)
401+
| Self::ManifestUtf8(..) => true,
401402
Self::Stream { .. } => true,
402403
Self::RetriedError { err, .. } => err.should_try_next_url(),
403404
err => {
@@ -856,13 +857,80 @@ where
856857

857858
#[cfg(test)]
858859
mod tests {
860+
use serde_json::json;
859861
use std::io::Write;
860-
use std::net::TcpListener;
861-
use uv_client::{BaseClientBuilder, retryable_on_request_failure};
862+
use uv_client::{BaseClientBuilder, fetch_with_url_fallback, retryable_on_request_failure};
862863
use uv_redacted::DisplaySafeUrl;
864+
use wiremock::matchers::{method, path};
865+
use wiremock::{Mock, MockServer, ResponseTemplate};
863866

864867
use super::*;
865868

869+
async fn spawn_manifest_server(response: ResponseTemplate) -> (DisplaySafeUrl, MockServer) {
870+
let server = MockServer::start().await;
871+
Mock::given(method("GET"))
872+
.and(path("/uv.ndjson"))
873+
.respond_with(response)
874+
.mount(&server)
875+
.await;
876+
877+
(
878+
DisplaySafeUrl::parse(&format!("{}/uv.ndjson", server.uri())).unwrap(),
879+
server,
880+
)
881+
}
882+
883+
fn manifest_response(body: &str) -> ResponseTemplate {
884+
ResponseTemplate::new(200).set_body_raw(body.to_owned(), "application/x-ndjson")
885+
}
886+
887+
fn not_found_response() -> ResponseTemplate {
888+
ResponseTemplate::new(404)
889+
}
890+
891+
fn uv_manifest_line(version: &str, platform: &str) -> String {
892+
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
893+
let url = format!(
894+
"https://github.com/astral-sh/uv/releases/download/{version}/uv-{platform}.{extension}"
895+
);
896+
897+
format!(
898+
"{}\n",
899+
json!({
900+
"version": version,
901+
"date": "2025-01-01T00:00:00Z",
902+
"artifacts": [{
903+
"platform": platform,
904+
"url": url,
905+
"archive_format": extension,
906+
}],
907+
})
908+
)
909+
}
910+
911+
async fn resolve_version_from_manifest_urls(
912+
urls: &[DisplaySafeUrl],
913+
constraints: Option<&VersionSpecifiers>,
914+
) -> Result<ResolvedVersion, Error> {
915+
let platform = Platform::from_env().unwrap();
916+
let platform_name = platform.as_cargo_dist_triple();
917+
let client_builder = BaseClientBuilder::default().retries(0);
918+
let retry_policy = client_builder.retry_policy();
919+
let client = client_builder.build();
920+
921+
fetch_with_url_fallback(urls, retry_policy, "manifest for `uv`", |url| {
922+
fetch_and_find_matching_version(
923+
Binary::Uv,
924+
constraints,
925+
None,
926+
&platform_name,
927+
url,
928+
&client,
929+
)
930+
})
931+
.await
932+
}
933+
866934
#[test]
867935
fn test_uv_download_urls() {
868936
let urls = Binary::Uv
@@ -886,6 +954,70 @@ mod tests {
886954
);
887955
}
888956

957+
#[tokio::test]
958+
async fn test_manifest_falls_back_on_404() {
959+
let platform = Platform::from_env().unwrap();
960+
let platform_name = platform.as_cargo_dist_triple();
961+
let (mirror_url, mirror_server) = spawn_manifest_server(not_found_response()).await;
962+
let (canonical_url, canonical_server) = spawn_manifest_server(manifest_response(
963+
&uv_manifest_line("1.2.3", &platform_name),
964+
))
965+
.await;
966+
967+
let resolved = resolve_version_from_manifest_urls(&[mirror_url, canonical_url], None)
968+
.await
969+
.expect("404 from mirror should fall back to canonical manifest");
970+
971+
assert_eq!(resolved.version, Version::new([1, 2, 3]));
972+
assert_eq!(mirror_server.received_requests().await.unwrap().len(), 1);
973+
assert_eq!(canonical_server.received_requests().await.unwrap().len(), 1);
974+
}
975+
976+
#[tokio::test]
977+
async fn test_manifest_falls_back_on_parse_error() {
978+
let platform = Platform::from_env().unwrap();
979+
let platform_name = platform.as_cargo_dist_triple();
980+
let (mirror_url, mirror_server) =
981+
spawn_manifest_server(manifest_response("{not json}\n")).await;
982+
let (canonical_url, canonical_server) = spawn_manifest_server(manifest_response(
983+
&uv_manifest_line("1.2.3", &platform_name),
984+
))
985+
.await;
986+
987+
let resolved = resolve_version_from_manifest_urls(&[mirror_url, canonical_url], None)
988+
.await
989+
.expect("parse failure from mirror should fall back to canonical manifest");
990+
991+
assert_eq!(resolved.version, Version::new([1, 2, 3]));
992+
assert_eq!(mirror_server.received_requests().await.unwrap().len(), 1);
993+
assert_eq!(canonical_server.received_requests().await.unwrap().len(), 1);
994+
}
995+
996+
#[tokio::test]
997+
async fn test_manifest_no_matching_version_does_not_fallback() {
998+
let platform = Platform::from_env().unwrap();
999+
let platform_name = platform.as_cargo_dist_triple();
1000+
let (mirror_url, mirror_server) = spawn_manifest_server(manifest_response(
1001+
&uv_manifest_line("1.2.3", &platform_name),
1002+
))
1003+
.await;
1004+
let (canonical_url, canonical_server) = spawn_manifest_server(manifest_response(
1005+
&uv_manifest_line("9.9.9", &platform_name),
1006+
))
1007+
.await;
1008+
let constraints =
1009+
VersionSpecifiers::from(VersionSpecifier::equals_version(Version::new([9, 9, 9])));
1010+
1011+
let err =
1012+
resolve_version_from_manifest_urls(&[mirror_url, canonical_url], Some(&constraints))
1013+
.await
1014+
.expect_err("no matching version should not fall back to canonical manifest");
1015+
1016+
assert!(matches!(err, Error::NoMatchingVersion { .. }));
1017+
assert_eq!(mirror_server.received_requests().await.unwrap().len(), 1);
1018+
assert_eq!(canonical_server.received_requests().await.unwrap().len(), 0);
1019+
}
1020+
8891021
/// Verify that `should_try_next_url` returns `true` even for streaming errors
8901022
/// that `retryable_on_request_failure` does not recognise as transient.
8911023
///
@@ -895,7 +1027,7 @@ mod tests {
8951027
async fn test_non_retryable_stream_error_triggers_url_fallback() {
8961028
use futures::TryStreamExt;
8971029

898-
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
1030+
let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap();
8991031
let addr = listener.local_addr().unwrap();
9001032

9011033
std::thread::spawn(move || {

0 commit comments

Comments
 (0)