Skip to content

Commit d8f7c8b

Browse files
committed
Make official uv self-update manifest fetching mirror-first
1 parent e8f3afe commit d8f7c8b

File tree

1 file changed

+179
-11
lines changed
  • crates/uv-bin-install/src

1 file changed

+179
-11
lines changed

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

Lines changed: 179 additions & 11 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,98 @@ where
856857

857858
#[cfg(test)]
858859
mod tests {
859-
use std::io::Write;
860+
use std::io::{Read, Write};
860861
use std::net::TcpListener;
861-
use uv_client::{BaseClientBuilder, retryable_on_request_failure};
862+
use std::sync::Arc;
863+
use std::sync::atomic::{AtomicUsize, Ordering};
864+
use std::sync::mpsc::{self, Sender};
865+
use std::thread::JoinHandle;
866+
use std::time::Duration;
867+
use uv_client::{BaseClientBuilder, fetch_with_url_fallback, retryable_on_request_failure};
862868
use uv_redacted::DisplaySafeUrl;
863869

864870
use super::*;
865871

872+
fn spawn_http_server(
873+
response: String,
874+
) -> (DisplaySafeUrl, Arc<AtomicUsize>, Sender<()>, JoinHandle<()>) {
875+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
876+
listener.set_nonblocking(true).unwrap();
877+
let addr = listener.local_addr().unwrap();
878+
let requests = Arc::new(AtomicUsize::new(0));
879+
let requests_clone = Arc::clone(&requests);
880+
let (shutdown_tx, shutdown_rx) = mpsc::channel();
881+
let handle = std::thread::spawn(move || {
882+
loop {
883+
if shutdown_rx.try_recv().is_ok() {
884+
return;
885+
}
886+
887+
match listener.accept() {
888+
Ok((mut stream, _)) => {
889+
requests_clone.fetch_add(1, Ordering::SeqCst);
890+
// Drain the request; we don't inspect it in these tests.
891+
let mut buf = [0u8; 4096];
892+
let _ = stream.read(&mut buf);
893+
stream.write_all(response.as_bytes()).unwrap();
894+
return;
895+
}
896+
Err(err) if err.kind() == io::ErrorKind::WouldBlock => {
897+
std::thread::sleep(Duration::from_millis(10));
898+
}
899+
Err(err) => panic!("failed to accept connection: {err}"),
900+
}
901+
}
902+
});
903+
(
904+
DisplaySafeUrl::parse(&format!("http://{addr}/uv.ndjson")).unwrap(),
905+
requests,
906+
shutdown_tx,
907+
handle,
908+
)
909+
}
910+
911+
fn manifest_response(body: &str) -> String {
912+
format!(
913+
"HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/x-ndjson\r\n\r\n{body}",
914+
body.len()
915+
)
916+
}
917+
918+
fn not_found_response() -> String {
919+
"HTTP/1.1 404 Not Found\r\nContent-Length: 0\r\n\r\n".to_string()
920+
}
921+
922+
fn uv_manifest_line(version: &str, platform: &str) -> String {
923+
let extension = if cfg!(windows) { "zip" } else { "tar.gz" };
924+
format!(
925+
"{{\"version\":\"{version}\",\"date\":\"2025-01-01T00:00:00Z\",\"artifacts\":[{{\"platform\":\"{platform}\",\"url\":\"https://github.com/astral-sh/uv/releases/download/{version}/uv-{platform}.{extension}\",\"archive_format\":\"{extension}\"}}]}}\n"
926+
)
927+
}
928+
929+
async fn resolve_version_from_manifest_urls(
930+
urls: &[DisplaySafeUrl],
931+
constraints: Option<&VersionSpecifiers>,
932+
) -> Result<ResolvedVersion, Error> {
933+
let platform = Platform::from_env().unwrap();
934+
let platform_name = platform.as_cargo_dist_triple();
935+
let client_builder = BaseClientBuilder::default().retries(0);
936+
let retry_policy = client_builder.retry_policy();
937+
let client = client_builder.build();
938+
939+
fetch_with_url_fallback(urls, retry_policy, "manifest for `uv`", |url| {
940+
fetch_and_find_matching_version(
941+
Binary::Uv,
942+
constraints,
943+
None,
944+
&platform_name,
945+
url,
946+
&client,
947+
)
948+
})
949+
.await
950+
}
951+
866952
#[test]
867953
fn test_uv_download_urls() {
868954
let urls = Binary::Uv
@@ -886,6 +972,88 @@ mod tests {
886972
);
887973
}
888974

975+
#[tokio::test]
976+
async fn test_manifest_falls_back_on_404() {
977+
let platform = Platform::from_env().unwrap();
978+
let platform_name = platform.as_cargo_dist_triple();
979+
let (mirror_url, mirror_requests, mirror_shutdown, mirror_handle) =
980+
spawn_http_server(not_found_response());
981+
let (canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
982+
spawn_http_server(manifest_response(&uv_manifest_line(
983+
"1.2.3",
984+
&platform_name,
985+
)));
986+
987+
let resolved = resolve_version_from_manifest_urls(&[mirror_url, canonical_url], None)
988+
.await
989+
.expect("404 from mirror should fall back to canonical manifest");
990+
991+
let _ = mirror_shutdown.send(());
992+
let _ = canonical_shutdown.send(());
993+
mirror_handle.join().unwrap();
994+
canonical_handle.join().unwrap();
995+
996+
assert_eq!(resolved.version, Version::new([1, 2, 3]));
997+
assert_eq!(mirror_requests.load(Ordering::SeqCst), 1);
998+
assert_eq!(canonical_requests.load(Ordering::SeqCst), 1);
999+
}
1000+
1001+
#[tokio::test]
1002+
async fn test_manifest_falls_back_on_parse_error() {
1003+
let platform = Platform::from_env().unwrap();
1004+
let platform_name = platform.as_cargo_dist_triple();
1005+
let (mirror_url, mirror_requests, mirror_shutdown, mirror_handle) =
1006+
spawn_http_server(manifest_response("{not json}\n"));
1007+
let (canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
1008+
spawn_http_server(manifest_response(&uv_manifest_line(
1009+
"1.2.3",
1010+
&platform_name,
1011+
)));
1012+
1013+
let resolved = resolve_version_from_manifest_urls(&[mirror_url, canonical_url], None)
1014+
.await
1015+
.expect("parse failure from mirror should fall back to canonical manifest");
1016+
1017+
let _ = mirror_shutdown.send(());
1018+
let _ = canonical_shutdown.send(());
1019+
mirror_handle.join().unwrap();
1020+
canonical_handle.join().unwrap();
1021+
1022+
assert_eq!(resolved.version, Version::new([1, 2, 3]));
1023+
assert_eq!(mirror_requests.load(Ordering::SeqCst), 1);
1024+
assert_eq!(canonical_requests.load(Ordering::SeqCst), 1);
1025+
}
1026+
1027+
#[tokio::test]
1028+
async fn test_manifest_no_matching_version_does_not_fallback() {
1029+
let platform = Platform::from_env().unwrap();
1030+
let platform_name = platform.as_cargo_dist_triple();
1031+
let (mirror_url, mirror_requests, mirror_shutdown, mirror_handle) = spawn_http_server(
1032+
manifest_response(&uv_manifest_line("1.2.3", &platform_name)),
1033+
);
1034+
let (canonical_url, canonical_requests, canonical_shutdown, canonical_handle) =
1035+
spawn_http_server(manifest_response(&uv_manifest_line(
1036+
"9.9.9",
1037+
&platform_name,
1038+
)));
1039+
let constraints =
1040+
VersionSpecifiers::from(VersionSpecifier::equals_version(Version::new([9, 9, 9])));
1041+
1042+
let err =
1043+
resolve_version_from_manifest_urls(&[mirror_url, canonical_url], Some(&constraints))
1044+
.await
1045+
.expect_err("no matching version should not fall back to canonical manifest");
1046+
1047+
let _ = mirror_shutdown.send(());
1048+
let _ = canonical_shutdown.send(());
1049+
mirror_handle.join().unwrap();
1050+
canonical_handle.join().unwrap();
1051+
1052+
assert!(matches!(err, Error::NoMatchingVersion { .. }));
1053+
assert_eq!(mirror_requests.load(Ordering::SeqCst), 1);
1054+
assert_eq!(canonical_requests.load(Ordering::SeqCst), 0);
1055+
}
1056+
8891057
/// Verify that `should_try_next_url` returns `true` even for streaming errors
8901058
/// that `retryable_on_request_failure` does not recognise as transient.
8911059
///

0 commit comments

Comments
 (0)