use std::process::Command;
use serde_json::{json, Map, Value};
use crate::runtimes::Catalog;
pub fn json(catalog: &Catalog) -> Value {
let mut runtimes: Map<String, Value> = Map::new();
for rt in crate::linked::runtimes() {
runtimes.insert(rt.info().id.into(), rt.compat());
}
for id in catalog.shipped_ids() {
if runtimes.contains_key(id.name()) {
continue;
}
let Some(worker) = catalog.shipped(id) else { continue };
let mut cmd = Command::new(&worker);
crate::exec::without_secrets(&mut cmd).args(["compat", "--json"]);
let answer = match crate::exec::output_within(&mut cmd, crate::exec::ANSWER_WITHIN) {
Ok(None) => json!({"error": format!("{} compat did not answer within {}s", worker.display(), crate::exec::ANSWER_WITHIN.as_secs())}),
Ok(Some(out)) if out.status.success() => serde_json::from_slice::<Value>(&out.stdout)
.unwrap_or_else(|e| json!({"error": format!("{}: compat is not JSON: {e}", worker.display())})),
Ok(Some(out)) => json!({"error": format!("{} compat exited {}{}", worker.display(), out.status, crate::exec::stderr_tail(&out.stderr))}),
Err(e) => json!({"error": format!("{}: {e}", worker.display())}),
};
runtimes.insert(id.name().into(), answer);
}
json!({"superfluid": env!("CARGO_PKG_VERSION"), "runtimes": runtimes})
}
pub fn unanswered(c: &Value) -> Vec<&str> {
c["runtimes"].as_object().map(|r| r.iter().filter(|(_, v)| v["error"].is_string()).map(|(id, _)| id.as_str()).collect()).unwrap_or_default()
}
pub fn text(c: &Value) -> String {
let mut out = format!("{}\n", crate::cli::version());
let Some(runtimes) = c["runtimes"].as_object() else { return out };
for (id, r) in runtimes {
if let Some(e) = r["error"].as_str() {
out.push_str(&format!("{id}: {e}\n"));
continue;
}
let line = match id.as_str() {
"basert" => {
let abi = r["abi"].as_u64().map(|n| format!("engine ABI {n}")).unwrap_or_else(|| "no engine ABI number (a header before 0.4.0)".into());
let unnumbered = match r["unnumbered_minor"].as_str() {
Some(minor) => format!("one that states no number serves if it is baseRT {minor}.x"),
None => "one that states no number does not serve".to_string(),
};
let pinned: Vec<String> = r["pinned_engines"]
.as_array()
.map(|a| a.iter().filter_map(|p| p["version"].as_str().map(str::to_string)).collect())
.unwrap_or_default();
format!(
"built against baseRT {} headers, {abi}. A libbaseRT with the same ABI number serves, whatever its version; {unnumbered}. \
Installs on its own: {}",
r["basert_version"].as_str().unwrap_or("?"),
if pinned.is_empty() { "nothing".to_string() } else { pinned.join(", ") }
)
}
_ => {
let tested: Vec<String> =
r["tested"].as_array().map(|a| a.iter().filter_map(|v| v.as_str().map(str::to_string)).collect()).unwrap_or_default();
let python = r["python"].as_str().map(|p| format!(" on Python {p}")).unwrap_or_default();
format!("installs on its own{python}: {}", if tested.is_empty() { "nothing".to_string() } else { tested.join(", ") })
}
};
out.push_str(&format!("{id}: {line}\n"));
}
out
}
#[cfg(test)]
mod tests {
use super::*;
use std::os::unix::fs::PermissionsExt;
use std::path::PathBuf;
fn scratch(name: &str) -> PathBuf {
let dir = std::env::temp_dir().join(format!("superfluid-compat-{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
dir
}
#[test]
fn every_shipped_adapter_answers_for_its_runtime() {
let root = scratch("shipped");
let beside = root.join("bin");
let libexec = root.join("libexec").join("superfluid");
std::fs::create_dir_all(&beside).unwrap();
std::fs::create_dir_all(&libexec).unwrap();
let toy = libexec.join("superfluid-worker-toyrt");
std::fs::write(&toy, "#!/bin/sh\n[ \"$1\" = compat ] || exit 2\necho \"{\\\"tested\\\":[\\\"t2\\\",\\\"t1\\\"],\\\"saw\\\":\\\"${SUPERFLUID_COMPAT_SENTINEL_TOKEN:-}\\\"}\"\n").unwrap();
std::fs::set_permissions(&toy, std::fs::Permissions::from_mode(0o755)).unwrap();
let broken = libexec.join("superfluid-worker-oldrt");
std::fs::write(&broken, "#!/bin/sh\nexit 2\n").unwrap();
std::fs::set_permissions(&broken, std::fs::Permissions::from_mode(0o755)).unwrap();
let fails = libexec.join("superfluid-worker-failrt");
std::fs::write(&fails, "#!/bin/sh\necho 'superfluid-worker-failrt: no recipe.json beside the adapter' >&2\nexit 1\n").unwrap();
std::fs::set_permissions(&fails, std::fs::Permissions::from_mode(0o755)).unwrap();
let cat = Catalog::new(root.join("home"), Some(beside.clone()), beside.join("superfluid-workerd"));
std::env::set_var("SUPERFLUID_COMPAT_SENTINEL_TOKEN", "hunter2");
let c = json(&cat);
std::env::remove_var("SUPERFLUID_COMPAT_SENTINEL_TOKEN");
assert_eq!(c["superfluid"], env!("CARGO_PKG_VERSION"));
assert_eq!(c["runtimes"]["toyrt"]["tested"][0], "t2");
assert_eq!(c["runtimes"]["toyrt"]["saw"], "", "a credential-named variable is not handed to the worker");
for id in crate::linked::ids() {
assert!(c["runtimes"][id.name()].is_object(), "the linked {} adapter answers in process: {c}", id.name());
}
assert!(c["runtimes"]["oldrt"]["error"].as_str().unwrap().contains("exited"), "{c}");
assert_eq!(c["runtimes"]["failrt"]["error"].as_str().unwrap().rsplit(": ").next(), Some("no recipe.json beside the adapter"), "{c}");
assert_eq!(unanswered(&c), ["failrt", "oldrt"], "{c}");
#[cfg(feature = "basert")]
{
assert!(c["runtimes"]["basert"]["basert_version"].is_string(), "{c}");
assert!(c["runtimes"]["basert"]["pinned_engines"].is_array(), "{c}");
}
let t = text(&c);
assert!(t.contains("toyrt: installs on its own: t2, t1"), "{t}");
assert!(t.contains("oldrt: ") && t.contains("exited"), "{t}");
assert!(unanswered(&json!({"runtimes": {"toyrt": {"tested": []}}})).is_empty());
let _ = std::fs::remove_dir_all(&root);
}
}