//! Stream native compiler events and run the resulting components through the process interface. use std::io::{self, Write}; use wasi::http::{outgoing_handler, types::*}; #[derive(serde::Deserialize)] #[serde(tag = "event", rename_all = "snake_case")] enum Event { Stdout { bytes: Vec, }, Stderr { bytes: Vec, }, Exit { code: i32, executables: Vec, }, } #[derive(serde::Deserialize)] struct Executable { path: String, cwd: String, } fn error(value: impl std::fmt::Debug) -> io::Error { io::Error::other(format!("{value:?}")) } fn local_help(argv: &[String]) -> Option<&'static str> { matches!(argv.first().map(String::as_str), Some("--help" | "-h" | "help")).then_some( "Usage: cargo [OPTIONS]\n\nWorld commands: build, check, clippy, test, run, fmt, fetch, add, remove, new, init\n", ) } pub fn run() -> io::Result { let argv: Vec = std::env::args().skip(1).collect(); if let Some(help) = local_help(&argv) { io::stdout().write_all(help.as_bytes())?; return Ok(0); } let data = serde_json::to_vec( &serde_json::json!({"argv": argv, "cwd": std::env::current_dir()?.to_string_lossy()}), )?; let fields = Fields::new(); fields .set("content-length", &[data.len().to_string().into_bytes()]) .map_err(error)?; fields .set("content-type", &[b"application/json".to_vec()]) .map_err(error)?; let request = OutgoingRequest::new(fields); request.set_method(&Method::Post).map_err(error)?; request.set_scheme(Some(&Scheme::Http)).map_err(error)?; request.set_authority(Some("host.sausage")).map_err(error)?; request.set_path_with_query(Some("/cargo")).map_err(error)?; let outgoing = request.body().map_err(error)?; let options = RequestOptions::new(); options .set_between_bytes_timeout(Some(1860 * 1_000_000_000)) .map_err(error)?; let future = outgoing_handler::handle(request, Some(options)).map_err(error)?; let stream = outgoing.write().map_err(error)?; let mut remaining = data.as_slice(); while !remaining.is_empty() { stream.subscribe().block(); let n = (stream.check_write().map_err(error)? as usize).min(remaining.len()); stream.write(&remaining[..n]).map_err(error)?; remaining = &remaining[n..]; } drop(stream); OutgoingBody::finish(outgoing, None).map_err(error)?; future.subscribe().block(); let response = future .get() .ok_or_else(|| error("missing response"))? .map_err(error)? .map_err(error)?; let incoming = response.consume().map_err(error)?; let stream = incoming.stream().map_err(error)?; let mut pending = Vec::new(); let mut result = None; loop { stream.subscribe().block(); match stream.read(65536) { Ok(bytes) => pending.extend(bytes), Err(wasi::io::streams::StreamError::Closed) => break, Err(e) => return Err(error(e)), } while let Some(end) = pending.iter().position(|b| *b == b'\n') { let line: Vec<_> = pending.drain(..=end).collect(); match serde_json::from_slice::(&line)? { Event::Stdout { bytes } => { io::stdout().write_all(&bytes)?; io::stdout().flush()?; } Event::Stderr { bytes } => { io::stderr().write_all(&bytes)?; io::stderr().flush()?; } Event::Exit { code, executables } => result = Some((code, executables)), } } } drop(stream); drop(IncomingBody::finish(incoming)); drop(response); let (code, executables) = result.ok_or_else(|| error("missing Cargo exit event"))?; if code != 0 { return Ok(code); } let action = argv.first().map(String::as_str).unwrap_or(""); let args = execution_args(&argv); for executable in executables { let code = spawn::Command::new(executable.path) .current_dir(executable.cwd) .args(&args) .status()?; if code != 0 { return Ok(if action == "test" { 101 } else { code }); } } Ok(0) } #[cfg(test)] #[test] fn help_is_local_and_lists_world_commands() { for flag in ["--help", "-h", "help"] { let help = local_help(&[flag.into()]).expect("local help"); assert!(help.starts_with("Usage: cargo [OPTIONS]\n")); assert!( help.contains("build, check, clippy, test, run, fmt, fetch, add, remove, new, init") ); } assert!(local_help(&["build".into()]).is_none()); assert!(local_help(&[]).is_none()); } fn execution_args(argv: &[String]) -> Vec { let split = argv.iter().position(|a| a == "--").unwrap_or(argv.len()); let mut args = argv.get(split + 1..).unwrap_or_default().to_vec(); if argv.first().is_some_and(|a| a == "test") { let mut value = false; for arg in &argv[1..split] { if !value && !arg.starts_with('-') { args.insert(0, arg.clone()); break; } value = [ "-p", "--package", "--test", "--bin", "--example", "--features", "--profile", "--manifest-path", "--exclude", "-j", "--jobs", "-F", "--config", "--target", "--target-dir", "--message-format", "--color", "--lockfile-path", "-Z", ] .contains(&arg.as_str()); } } args }