169 lines
5.8 KiB
Rust
169 lines
5.8 KiB
Rust
//! 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<u8>,
|
|
},
|
|
Stderr {
|
|
bytes: Vec<u8>,
|
|
},
|
|
Exit {
|
|
code: i32,
|
|
executables: Vec<Executable>,
|
|
},
|
|
}
|
|
#[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 <COMMAND> [OPTIONS]\n\nWorld commands: build, check, clippy, test, run, fmt, fetch, add, remove, new, init\n",
|
|
)
|
|
}
|
|
pub fn run() -> io::Result<i32> {
|
|
let argv: Vec<String> = 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::<Event>(&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 <COMMAND> [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<String> {
|
|
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
|
|
}
|