Files
sausage/userland/cargo/src/lib.rs
T

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
}