Skip to content

Commit 1366225

Browse files
committed
Auto merge of #10470 - arlosi:http, r=Eh2406
HTTP registry implementation Implement HTTP registry support described in [RFC 2789](rust-lang/rfcs#2789). Adds a new unstable flag `-Z http-registry` which allows cargo to interact with remote registries served over http rather than git. These registries can be identified by urls starting with `sparse+http://` or `sparse+https://`. When fetching index metadata over http, cargo only downloads the metadata for needed crates, which can save significant time and bandwidth over git. The format of the http index is identical to a checkout of a git-based index. This change is based on `@jonhoo's` PR #8890. cc `@Eh2406` Remaining items: - [x] Performance measurements - [x] Make unstable only - [x] Investigate unification of download system. Probably best done in separate change. - [x] Unify registry tests (code duplication in `http_registry.rs`) - [x] Use existing on-disk cache, rather than adding a new one.
2 parents c5509f8 + 412b633 commit 1366225

File tree

18 files changed

+1807
-323
lines changed

18 files changed

+1807
-323
lines changed

crates/cargo-test-support/src/registry.rs

+162-1
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,10 @@ use std::collections::BTreeMap;
77
use std::fmt::Write as _;
88
use std::fs::{self, File};
99
use std::io::{BufRead, BufReader, Write};
10-
use std::net::TcpListener;
10+
use std::net::{SocketAddr, TcpListener};
1111
use std::path::{Path, PathBuf};
12+
use std::sync::atomic::{AtomicBool, Ordering};
13+
use std::sync::Arc;
1214
use std::thread;
1315
use tar::{Builder, Header};
1416
use url::Url;
@@ -368,6 +370,165 @@ pub fn alt_init() {
368370
RegistryBuilder::new().alternative(true).build();
369371
}
370372

373+
pub struct RegistryServer {
374+
done: Arc<AtomicBool>,
375+
server: Option<thread::JoinHandle<()>>,
376+
addr: SocketAddr,
377+
}
378+
379+
impl RegistryServer {
380+
pub fn addr(&self) -> SocketAddr {
381+
self.addr
382+
}
383+
}
384+
385+
impl Drop for RegistryServer {
386+
fn drop(&mut self) {
387+
self.done.store(true, Ordering::SeqCst);
388+
// NOTE: we can't actually await the server since it's blocked in accept()
389+
let _ = self.server.take();
390+
}
391+
}
392+
393+
#[must_use]
394+
pub fn serve_registry(registry_path: PathBuf) -> RegistryServer {
395+
let listener = TcpListener::bind("127.0.0.1:0").unwrap();
396+
let addr = listener.local_addr().unwrap();
397+
let done = Arc::new(AtomicBool::new(false));
398+
let done2 = done.clone();
399+
400+
let t = thread::spawn(move || {
401+
let mut line = String::new();
402+
'server: while !done2.load(Ordering::SeqCst) {
403+
let (socket, _) = listener.accept().unwrap();
404+
// Let's implement a very naive static file HTTP server.
405+
let mut buf = BufReader::new(socket);
406+
407+
// First, the request line:
408+
// GET /path HTTPVERSION
409+
line.clear();
410+
if buf.read_line(&mut line).unwrap() == 0 {
411+
// Connection terminated.
412+
continue;
413+
}
414+
415+
assert!(line.starts_with("GET "), "got non-GET request: {}", line);
416+
let path = PathBuf::from(
417+
line.split_whitespace()
418+
.skip(1)
419+
.next()
420+
.unwrap()
421+
.trim_start_matches('/'),
422+
);
423+
424+
let file = registry_path.join(path);
425+
if file.exists() {
426+
// Grab some other headers we may care about.
427+
let mut if_modified_since = None;
428+
let mut if_none_match = None;
429+
loop {
430+
line.clear();
431+
if buf.read_line(&mut line).unwrap() == 0 {
432+
continue 'server;
433+
}
434+
435+
if line == "\r\n" {
436+
// End of headers.
437+
line.clear();
438+
break;
439+
}
440+
441+
let value = line
442+
.splitn(2, ':')
443+
.skip(1)
444+
.next()
445+
.map(|v| v.trim())
446+
.unwrap();
447+
448+
if line.starts_with("If-Modified-Since:") {
449+
if_modified_since = Some(value.to_owned());
450+
} else if line.starts_with("If-None-Match:") {
451+
if_none_match = Some(value.trim_matches('"').to_owned());
452+
}
453+
}
454+
455+
// Now grab info about the file.
456+
let data = fs::read(&file).unwrap();
457+
let etag = Sha256::new().update(&data).finish_hex();
458+
let last_modified = format!("{:?}", file.metadata().unwrap().modified().unwrap());
459+
460+
// Start to construct our response:
461+
let mut any_match = false;
462+
let mut all_match = true;
463+
if let Some(expected) = if_none_match {
464+
if etag != expected {
465+
all_match = false;
466+
} else {
467+
any_match = true;
468+
}
469+
}
470+
if let Some(expected) = if_modified_since {
471+
// NOTE: Equality comparison is good enough for tests.
472+
if last_modified != expected {
473+
all_match = false;
474+
} else {
475+
any_match = true;
476+
}
477+
}
478+
479+
// Write out the main response line.
480+
if any_match && all_match {
481+
buf.get_mut()
482+
.write_all(b"HTTP/1.1 304 Not Modified\r\n")
483+
.unwrap();
484+
} else {
485+
buf.get_mut().write_all(b"HTTP/1.1 200 OK\r\n").unwrap();
486+
}
487+
// TODO: Support 451 for crate index deletions.
488+
489+
// Write out other headers.
490+
buf.get_mut()
491+
.write_all(format!("Content-Length: {}\r\n", data.len()).as_bytes())
492+
.unwrap();
493+
buf.get_mut()
494+
.write_all(format!("ETag: \"{}\"\r\n", etag).as_bytes())
495+
.unwrap();
496+
buf.get_mut()
497+
.write_all(format!("Last-Modified: {}\r\n", last_modified).as_bytes())
498+
.unwrap();
499+
500+
// And finally, write out the body.
501+
buf.get_mut().write_all(b"\r\n").unwrap();
502+
buf.get_mut().write_all(&data).unwrap();
503+
} else {
504+
loop {
505+
line.clear();
506+
if buf.read_line(&mut line).unwrap() == 0 {
507+
// Connection terminated.
508+
continue 'server;
509+
}
510+
511+
if line == "\r\n" {
512+
break;
513+
}
514+
}
515+
516+
buf.get_mut()
517+
.write_all(b"HTTP/1.1 404 Not Found\r\n\r\n")
518+
.unwrap();
519+
buf.get_mut().write_all(b"\r\n").unwrap();
520+
}
521+
buf.get_mut().flush().unwrap();
522+
}
523+
});
524+
525+
RegistryServer {
526+
addr,
527+
server: Some(t),
528+
done,
529+
}
530+
}
531+
371532
/// Creates a new on-disk registry.
372533
pub fn init_registry(registry_path: PathBuf, dl_url: String, api_url: Url, api_path: PathBuf) {
373534
// Initialize a new registry.

src/cargo/core/features.rs

+2
Original file line numberDiff line numberDiff line change
@@ -650,6 +650,7 @@ unstable_cli_options!(
650650
no_index_update: bool = ("Do not update the registry index even if the cache is outdated"),
651651
panic_abort_tests: bool = ("Enable support to run tests with -Cpanic=abort"),
652652
host_config: bool = ("Enable the [host] section in the .cargo/config.toml file"),
653+
http_registry: bool = ("Support HTTP-based crate registries"),
653654
target_applies_to_host: bool = ("Enable the `target-applies-to-host` key in the .cargo/config.toml file"),
654655
rustdoc_map: bool = ("Allow passing external documentation mappings to rustdoc"),
655656
separate_nightlies: bool = (HIDDEN),
@@ -875,6 +876,7 @@ impl CliUnstable {
875876
"multitarget" => self.multitarget = parse_empty(k, v)?,
876877
"rustdoc-map" => self.rustdoc_map = parse_empty(k, v)?,
877878
"terminal-width" => self.terminal_width = Some(parse_usize_opt(v)?),
879+
"http-registry" => self.http_registry = parse_empty(k, v)?,
878880
"namespaced-features" => stabilized_warn(k, "1.60", STABILISED_NAMESPACED_FEATURES),
879881
"weak-dep-features" => stabilized_warn(k, "1.60", STABILIZED_WEAK_DEP_FEATURES),
880882
"credential-process" => self.credential_process = parse_empty(k, v)?,

src/cargo/core/package.rs

+2-9
Original file line numberDiff line numberDiff line change
@@ -405,13 +405,6 @@ impl<'cfg> PackageSet<'cfg> {
405405
) -> CargoResult<PackageSet<'cfg>> {
406406
// We've enabled the `http2` feature of `curl` in Cargo, so treat
407407
// failures here as fatal as it would indicate a build-time problem.
408-
//
409-
// Note that the multiplexing support is pretty new so we're having it
410-
// off-by-default temporarily.
411-
//
412-
// Also note that pipelining is disabled as curl authors have indicated
413-
// that it's buggy, and we've empirically seen that it's buggy with HTTP
414-
// proxies.
415408
let mut multi = Multi::new();
416409
let multiplexing = config.http_config()?.multiplexing.unwrap_or(true);
417410
multi
@@ -700,7 +693,7 @@ impl<'a, 'cfg> Downloads<'a, 'cfg> {
700693
return Ok(Some(pkg));
701694
}
702695

703-
// Ask the original source fo this `PackageId` for the corresponding
696+
// Ask the original source for this `PackageId` for the corresponding
704697
// package. That may immediately come back and tell us that the package
705698
// is ready, or it could tell us that it needs to be downloaded.
706699
let mut sources = self.set.sources.borrow_mut();
@@ -757,7 +750,7 @@ impl<'a, 'cfg> Downloads<'a, 'cfg> {
757750
// initiate dozens of connections to crates.io, but rather only one.
758751
// Once the main one is opened we realized that pipelining is possible
759752
// and multiplexing is possible with static.crates.io. All in all this
760-
// reduces the number of connections done to a more manageable state.
753+
// reduces the number of connections down to a more manageable state.
761754
try_old_curl!(handle.pipewait(true), "pipewait");
762755

763756
handle.write_function(move |buf| {

src/cargo/core/registry.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -180,6 +180,11 @@ impl<'cfg> PackageRegistry<'cfg> {
180180
}
181181

182182
self.load(namespace, kind)?;
183+
184+
// This isn't strictly necessary since it will be called later.
185+
// However it improves error messages for sources that issue errors
186+
// in `block_until_ready` because the callers here have context about
187+
// which deps are being resolved.
183188
self.block_until_ready()?;
184189
Ok(())
185190
}
@@ -273,7 +278,7 @@ impl<'cfg> PackageRegistry<'cfg> {
273278
// First up we need to actually resolve each `deps` specification to
274279
// precisely one summary. We're not using the `query` method below as it
275280
// internally uses maps we're building up as part of this method
276-
// (`patches_available` and `patches). Instead we're going straight to
281+
// (`patches_available` and `patches`). Instead we're going straight to
277282
// the source to load information from it.
278283
//
279284
// Remember that each dependency listed in `[patch]` has to resolve to

src/cargo/core/source/source_id.rs

+6-1
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,11 @@ impl SourceId {
135135
Ok(SourceId::new(SourceKind::Registry, url, None)?
136136
.with_precise(Some("locked".to_string())))
137137
}
138+
"sparse" => {
139+
let url = string.into_url()?;
140+
Ok(SourceId::new(SourceKind::Registry, url, None)?
141+
.with_precise(Some("locked".to_string())))
142+
}
138143
"path" => {
139144
let url = url.into_url()?;
140145
SourceId::new(SourceKind::Path, url, None)
@@ -301,7 +306,7 @@ impl SourceId {
301306
self,
302307
yanked_whitelist,
303308
config,
304-
))),
309+
)?)),
305310
SourceKind::LocalRegistry => {
306311
let path = match self.inner.url.to_file_path() {
307312
Ok(p) => p,

src/cargo/ops/registry.rs

+6-3
Original file line numberDiff line numberDiff line change
@@ -459,7 +459,7 @@ fn registry(
459459
}
460460
let api_host = {
461461
let _lock = config.acquire_package_cache_lock()?;
462-
let mut src = RegistrySource::remote(sid, &HashSet::new(), config);
462+
let mut src = RegistrySource::remote(sid, &HashSet::new(), config)?;
463463
// Only update the index if the config is not available or `force` is set.
464464
if force_update {
465465
src.invalidate_cache()
@@ -528,8 +528,11 @@ pub fn http_handle_and_timeout(config: &Config) -> CargoResult<(Easy, HttpTimeou
528528
specified"
529529
)
530530
}
531-
if !config.network_allowed() {
532-
bail!("can't make HTTP request in the offline mode")
531+
if config.offline() {
532+
bail!(
533+
"attempting to make an HTTP request, but --offline was \
534+
specified"
535+
)
533536
}
534537

535538
// The timeout option for libcurl by default times out the entire transfer,

src/cargo/sources/path.rs

+3-7
Original file line numberDiff line numberDiff line change
@@ -498,9 +498,7 @@ impl<'cfg> Debug for PathSource<'cfg> {
498498

499499
impl<'cfg> Source for PathSource<'cfg> {
500500
fn query(&mut self, dep: &Dependency, f: &mut dyn FnMut(Summary)) -> Poll<CargoResult<()>> {
501-
if !self.updated {
502-
return Poll::Pending;
503-
}
501+
self.update()?;
504502
for s in self.packages.iter().map(|p| p.summary()) {
505503
if dep.matches(s) {
506504
f(s.clone())
@@ -514,9 +512,7 @@ impl<'cfg> Source for PathSource<'cfg> {
514512
_dep: &Dependency,
515513
f: &mut dyn FnMut(Summary),
516514
) -> Poll<CargoResult<()>> {
517-
if !self.updated {
518-
return Poll::Pending;
519-
}
515+
self.update()?;
520516
for s in self.packages.iter().map(|p| p.summary()) {
521517
f(s.clone())
522518
}
@@ -537,7 +533,7 @@ impl<'cfg> Source for PathSource<'cfg> {
537533

538534
fn download(&mut self, id: PackageId) -> CargoResult<MaybePackage> {
539535
trace!("getting packages; id={}", id);
540-
536+
self.update()?;
541537
let pkg = self.packages.iter().find(|pkg| pkg.package_id() == id);
542538
pkg.cloned()
543539
.map(MaybePackage::Ready)

0 commit comments

Comments
 (0)