use std::{
collections::{BTreeSet, HashMap, HashSet, VecDeque},
path::Path,
};
use cargo_metadata::{DependencyKind, MetadataCommand};
use cfg_expr::{targets::get_builtin_target_by_triple, Expression, Predicate};
use serde::Serialize;
use toml::{Table, Value};
use crate::error::Error;
#[derive(Clone, Debug, Default, Serialize)]
pub struct MetadataNode {
table: Table,
parents: BTreeSet<String>,
children: usize,
}
impl MetadataNode {
fn new(value: impl Serialize) -> Result<Self, Error> {
let mut table = Table::try_from(value)?;
filter_cfg(&mut table)?;
Ok(Self {
table,
..Default::default()
})
}
}
fn filter_cfg(table: &mut Table) -> Result<(), Error> {
let target = get_builtin_target_by_triple(env!("SYSTEM_DEPS_TARGET"))
.expect("The target set by the build script should be valid");
filter_cfg_inner(table, target)
}
fn filter_cfg_inner(
table: &mut Table,
target: &cfg_expr::targets::TargetInfo,
) -> Result<(), Error> {
let cfg_keys: Vec<String> = table
.keys()
.filter(|k| k.starts_with("cfg(") && k.ends_with(")"))
.cloned()
.collect();
let mut cond = Table::new();
for key in cfg_keys {
let value = table.remove(&key).unwrap();
let pred = key
.strip_prefix("cfg(")
.and_then(|s| s.strip_suffix(")"))
.unwrap();
let expr = Expression::parse(pred).map_err(Error::InvalidCfg)?;
let res = expr.eval(|pred| match pred {
Predicate::Target(p) => Some(p.matches(target)),
_ => None,
});
if !res.ok_or(Error::UnsupportedCfg(pred.into()))? {
continue;
}
let Value::Table(value) = value else {
return Err(Error::CfgNotObject(pred.into()));
};
merge(&mut cond, value, false)?;
}
merge(table, cond, true)?;
let keys: Vec<String> = table.keys().cloned().collect();
for key in keys {
if let Some(Value::Table(sub)) = table.get_mut(&key) {
filter_cfg_inner(sub, target)?;
}
}
Ok(())
}
pub fn read_metadata(
manifest: impl AsRef<Path>,
section: &str,
merge: impl Fn(&mut Table, Table, bool) -> Result<(), Error>,
) -> Result<Table, Error> {
let manifest = manifest.as_ref();
let nodes = collect_nodes(manifest, section)?;
reduce_nodes(nodes, &merge)
}
fn collect_nodes(manifest: &Path, section: &str) -> Result<HashMap<String, MetadataNode>, Error> {
use std::iter;
let data = run_cargo_metadata(manifest)?;
let value = data
.workspace_metadata
.get(section)
.cloned()
.unwrap_or_default();
let root_node = MetadataNode::new(value).unwrap_or_default();
let mut packages: VecDeque<_> = if let Some(root) = data.root_package() {
[(root, "")].into()
} else {
data.workspace_packages()
.into_iter()
.zip(iter::repeat(""))
.collect()
};
let mut nodes = HashMap::from([("".to_string(), root_node)]);
let mut visited = HashSet::new();
while let Some((pkg, parent)) = packages.pop_front() {
let name = pkg.name.as_str();
if !visited.insert(name) {
if let Some(node) = nodes.get_mut(name) {
if node.parents.insert(parent.into()) {
if let Some(p) = nodes.get_mut(parent) {
p.children += 1
}
}
}
continue;
}
if pkg.manifest_path.starts_with(manifest.parent().unwrap()) {
println!("cargo:rerun-if-changed={}", pkg.manifest_path);
};
let node = match (nodes.get_mut(name), pkg.metadata.get(section).cloned()) {
(None, Some(s)) => {
nodes.insert(name.into(), MetadataNode::new(s)?);
nodes.get_mut(name)
}
(n, _) => n,
};
let next_parent = if let Some(node) = node {
if node.parents.insert(parent.into()) {
if let Some(p) = nodes.get_mut(parent) {
p.children += 1
}
}
name
} else {
parent
};
for dep in &pkg.dependencies {
if !matches!(dep.kind, DependencyKind::Normal) {
continue;
}
if let Some(dep_pkg) = data
.packages
.iter()
.find(|p| p.name.as_str() == dep.name.as_str())
{
packages.push_back((dep_pkg, next_parent));
};
}
}
Ok(nodes)
}
fn run_cargo_metadata(manifest: &Path) -> Result<cargo_metadata::Metadata, Error> {
#[cfg(not(windows))]
{
MetadataCommand::new()
.manifest_path(manifest)
.exec()
.map_err(|e| Error::PackageNotFound(format!("cargo metadata: {e}")))
}
#[cfg(windows)]
{
let mut cmd = MetadataCommand::new();
cmd.manifest_path(manifest);
let mut raw = cmd.cargo_command();
let tmp_target = std::env::temp_dir().join("system-deps-meta-cargo-metadata");
let _ = std::fs::create_dir_all(&tmp_target);
raw.env("CARGO_TARGET_DIR", &tmp_target);
let output = raw
.output()
.map_err(|e| Error::PackageNotFound(format!("cargo metadata: {e}")))?;
if !output.status.success() {
return Err(Error::PackageNotFound(format!(
"cargo metadata failed: {}",
String::from_utf8_lossy(&output.stderr)
)));
}
let stdout = std::str::from_utf8(&output.stdout)
.map_err(|e| Error::PackageNotFound(format!("cargo metadata utf8: {e}")))?;
MetadataCommand::parse(stdout)
.map_err(|e| Error::PackageNotFound(format!("parse cargo metadata: {e}")))
}
}
fn reduce_nodes(
nodes: HashMap<String, MetadataNode>,
merge: &impl Fn(&mut Table, Table, bool) -> Result<(), Error>,
) -> Result<Table, Error> {
let mut res = Table::new();
let mut curr = Table::new();
let mut queue = VecDeque::new();
let nodes: HashMap<&str, MetadataNode> = nodes
.iter()
.filter_map(|(k, v)| {
if v.children == 0 {
queue.push_back(v.clone());
None
} else {
Some((k.as_str(), v.clone()))
}
})
.collect();
while let Some(node) = queue.pop_front() {
for p in node.parents.iter().rev() {
let Some(parent) = nodes.get(p.as_str()) else {
return Err(Error::PackageNotFound(p.into()));
};
queue.push_front(parent.clone());
}
merge(&mut curr, node.table, true)?;
if node.parents.is_empty() {
merge(&mut res, curr, false)?;
curr = Table::new();
}
}
Ok(res)
}
pub fn merge(rhs: &mut Table, lhs: Table, force: bool) -> Result<(), Error> {
for (key, lhs) in lhs {
let Some(rhs) = rhs.get_mut(&key) else {
rhs.insert(key, lhs);
continue;
};
if *rhs == lhs {
continue;
}
if std::mem::discriminant(rhs) != std::mem::discriminant(&lhs) {
return Err(Error::IncompatibleMerge);
}
match (rhs, lhs) {
(Value::Array(rhs), Value::Array(lhs)) => {
for value in lhs {
if !rhs.contains(&value) {
rhs.push(value);
}
}
}
(Value::Table(rhs), Value::Table(lhs)) => {
merge(rhs, lhs, force)?;
}
(r, l) => {
if !force {
return Err(Error::IncompatibleMerge);
}
*r = l;
}
}
}
Ok(())
}