use std::collections::{BTreeSet, VecDeque};
use std::io;
use std::path::Path;
use std::process::{Command, Stdio};
use serde_json::Value;
fn metadata(dir: Option<&Path>) -> io::Result<Value> {
let mut cmd = Command::new("cargo");
cmd.args(["metadata", "--format-version", "1"]);
cmd.stdout(Stdio::piped());
cmd.stderr(Stdio::piped());
if let Some(d) = dir {
cmd.current_dir(d);
}
let out = cmd.output()?;
if !out.status.success() {
return Err(io::Error::other(format!(
"cargo metadata failed: {}",
String::from_utf8_lossy(&out.stderr).trim()
)));
}
Ok(serde_json::from_slice(&out.stdout)?)
}
pub fn local_deps(pkg: &str, dir: Option<&Path>) -> io::Result<Vec<String>> {
collect(pkg, &metadata(dir)?)
}
fn collect(pkg: &str, meta: &Value) -> io::Result<Vec<String>> {
let packages: Vec<&Value> = meta["packages"].as_array().into_iter().flatten().collect();
let members: BTreeSet<&str> = meta["workspace_members"]
.as_array()
.into_iter()
.flatten()
.filter_map(Value::as_str)
.collect();
let root = packages
.iter()
.find(|p| p["name"].as_str() == Some(pkg))
.and_then(|p| p["id"].as_str())
.ok_or_else(|| io::Error::other(format!("no workspace package named {pkg:?}")))?;
let mut seen: BTreeSet<&str> = BTreeSet::from([root]);
let mut queue = VecDeque::from([root]);
let mut found = BTreeSet::new();
while let Some(id) = queue.pop_front() {
for dep in normal_deps(meta, id) {
if !members.contains(dep) || !seen.insert(dep) {
continue;
}
queue.push_back(dep);
if let Some(p) = packages.iter().find(|p| p["id"].as_str() == Some(dep))
&& let (Some(name), true) = (p["name"].as_str(), has_lib_target(p))
{
found.insert(name.to_string());
}
}
}
Ok(found.into_iter().collect())
}
pub fn has_bench_target(pkg: &str, target: &str, dir: Option<&Path>) -> io::Result<bool> {
Ok(bench_target_in(&metadata(dir)?, pkg, target))
}
fn bench_target_in(meta: &Value, pkg: &str, target: &str) -> bool {
meta["packages"]
.as_array()
.into_iter()
.flatten()
.filter(|p| p["name"].as_str() == Some(pkg))
.flat_map(|p| p["targets"].as_array().into_iter().flatten())
.any(|t| {
t["name"].as_str() == Some(target)
&& t["kind"]
.as_array()
.into_iter()
.flatten()
.any(|k| k.as_str() == Some("bench"))
})
}
fn normal_deps<'a>(meta: &'a Value, id: &str) -> Vec<&'a str> {
let Some(node) = meta["resolve"]["nodes"]
.as_array()
.into_iter()
.flatten()
.find(|n| n["id"].as_str() == Some(id))
else {
return Vec::new();
};
node["deps"]
.as_array()
.into_iter()
.flatten()
.filter(|d| {
d["dep_kinds"]
.as_array()
.into_iter()
.flatten()
.any(|k| k["kind"].is_null())
})
.filter_map(|d| d["pkg"].as_str())
.collect()
}
fn has_lib_target(pkg: &Value) -> bool {
pkg["targets"].as_array().into_iter().flatten().any(|t| {
t["kind"]
.as_array()
.into_iter()
.flatten()
.any(|k| matches!(k.as_str(), Some("lib" | "rlib" | "dylib")))
})
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
fn lib(name: &str) -> Value {
json!({ "name": name, "id": name, "targets": [{ "kind": ["lib"], "name": name }] })
}
fn meta(packages: Vec<Value>, members: Vec<&str>, nodes: Vec<Value>) -> Value {
json!({ "packages": packages, "workspace_members": members,
"resolve": { "nodes": nodes } })
}
fn node(id: &str, deps: Vec<(&str, Value)>) -> Value {
let deps: Vec<Value> = deps
.into_iter()
.map(|(pkg, kinds)| json!({ "pkg": pkg, "dep_kinds": kinds }))
.collect();
json!({ "id": id, "deps": deps })
}
fn normal() -> Value {
json!([{ "kind": Value::Null }])
}
#[test]
fn a_registry_dependency_is_never_returned() {
let m = meta(
vec![lib("server"), lib("core"), lib("serde")],
vec!["server", "core"],
vec![node(
"server",
vec![("core", normal()), ("serde", normal())],
)],
);
assert_eq!(collect("server", &m).unwrap(), vec!["core".to_string()]);
}
#[test]
fn transitive_workspace_members_come_along() {
let m = meta(
vec![lib("server"), lib("core"), lib("model")],
vec!["server", "core", "model"],
vec![
node("server", vec![("core", normal())]),
node("core", vec![("model", normal())]),
],
);
assert_eq!(
collect("server", &m).unwrap(),
vec!["core".to_string(), "model".to_string()]
);
}
#[test]
fn dev_and_build_only_edges_are_skipped() {
let dev = json!([{ "kind": "dev" }]);
let m = meta(
vec![lib("server"), lib("fixtures")],
vec!["server", "fixtures"],
vec![node("server", vec![("fixtures", dev)])],
);
assert!(collect("server", &m).unwrap().is_empty());
}
#[test]
fn a_proc_macro_member_has_no_lib_to_document() {
let macros = json!({ "name": "macros", "id": "macros",
"targets": [{ "kind": ["proc-macro"], "name": "macros" }] });
let m = meta(
vec![lib("server"), macros],
vec!["server", "macros"],
vec![node("server", vec![("macros", normal())])],
);
assert!(collect("server", &m).unwrap().is_empty());
}
#[test]
fn a_dependency_cycle_terminates() {
let m = meta(
vec![lib("a"), lib("b")],
vec!["a", "b"],
vec![
node("a", vec![("b", normal())]),
node("b", vec![("a", normal())]),
],
);
assert_eq!(collect("a", &m).unwrap(), vec!["b".to_string()]);
}
#[test]
fn a_bench_target_is_found_only_on_the_package_that_has_one() {
let benched = json!({ "name": "server", "id": "server", "targets": [
{ "kind": ["lib"], "name": "server" },
{ "kind": ["bench"], "name": "soothfast" }] });
let m = meta(vec![benched, lib("plain")], vec!["server", "plain"], vec![]);
assert!(bench_target_in(&m, "server", "soothfast"));
assert!(!bench_target_in(&m, "plain", "soothfast"));
assert!(!bench_target_in(&m, "server", "criterion"));
assert!(!bench_target_in(&m, "absent", "soothfast"));
}
#[test]
fn an_unknown_package_is_an_error() {
let m = meta(vec![lib("server")], vec!["server"], vec![]);
assert!(collect("nope", &m).is_err());
}
}