use std::{borrow::Borrow, collections::BTreeSet, io};
use cargo_metadata::{camino::Utf8Path, CrateType, Target, TargetKind};
use io_extra::IoErrorExt as _;
use petgraph::{
algo::{has_path_connecting, DfsSpace},
prelude::DiGraphMap,
visit::NodeFiltered,
};
use serde::Deserialize;
pub fn mesonify<W: io::Write>(
unit_graph: UnitGraph,
strip_prefix: Option<&Utf8Path>,
writer: W,
) -> io::Result<()> {
let mut w = writer;
let UnitGraph { units, roots } = unit_graph;
let mut graph = DiGraphMap::from_edges(
units
.iter()
.enumerate()
.flat_map(|(ix, unit)| unit.dependencies.iter().map(move |dep| (dep.index, ix, ()))),
);
for ix in graph.nodes() {
if ix > units.len() {
return Err(io::Error::invalid_data("graph indices out of bounds"));
}
}
let (is_build_script, had_build_script) = graph
.all_edges()
.filter_map(|(from, to, ())| Some((from, units[from].is_build_script().then_some(to)?)))
.unzip::<_, _, BTreeSet<_>, BTreeSet<_>>();
for ix in is_build_script {
graph.remove_node(ix);
}
if roots.is_empty() {
return Err(io::Error::invalid_data("graph must have nonempty roots"));
};
let graph = NodeFiltered::from_fn(&graph, |node| {
let mut space = DfsSpace::new(&graph);
roots
.iter()
.any(|root| has_path_connecting(&graph, node, *root, Some(&mut space)))
});
writeln!(w, "rust = import('rust')")?;
writeln!(w)?;
writeln!(w, "# caller may pass more compiler args")?;
writeln!(w, "if not is_variable('unit2args')")?;
writeln!(w, " unit2args = {{}}")?;
writeln!(w, "endif")?;
writeln!(w)?;
writeln!(w, "# caller may override the name given to the unit")?;
writeln!(w, "if not is_variable('unit2name')")?;
writeln!(w, " unit2name = {{}}")?;
writeln!(w, "endif")?;
writeln!(w)?;
for ix in petgraph::algo::toposort(&graph, None)
.map_err(|_| io::Error::invalid_data("cycle in dependency graph"))?
{
let unit @ Unit {
pkg_id,
target:
Target {
name,
src_path,
edition,
..
},
features,
dependencies,
..
} = &units[ix];
let src_path = match strip_prefix {
Some(it) => src_path
.strip_prefix(it)
.map_err(io::Error::invalid_input)?,
None => src_path,
};
writeln!(w)?;
writeln!(w, "# {pkg_id}")?;
if had_build_script.contains(&ix) {
writeln!(w, "# (had build script)")?
}
let f = if unit.is_library() {
"library"
} else if unit.is_proc_macro() {
"rust.proc_macro"
} else {
return Err(io::Error::unsupported(format!("{unit:?}")));
};
writeln!(w, "unit{ix} = {f}(")?;
writeln!(w, " unit2name.get('{ix}', 'unit{ix}'), # {name}")?;
writeln!(w, " sources: structured_sources('{src_path}'),")?;
writeln!(w, " rust_args: [")?;
writeln!(w, " '--edition={edition}',")?;
for feature in features {
writeln!(w, " '--cfg', 'feature=\"{feature}\"',")?;
}
writeln!(w, " unit2args.get('{ix}', []),")?;
writeln!(w, " ],")?; let non_build_script_deps = dependencies.iter().filter_map(|it| {
let unit = &units[it.index];
match unit.is_build_script() {
true => None,
false => Some((unit, it)),
}
});
if non_build_script_deps.clone().count() != 0 {
writeln!(w, " link_with: [")?;
for (unit, dep) in non_build_script_deps.clone() {
writeln!(w, " unit{}, # {}", dep.index, unit.target.name)?
}
writeln!(w, " ],")?;
writeln!(w, " rust_dependency_map: {{")?;
for (
_,
Dependency {
index,
extern_crate_name,
},
) in non_build_script_deps
{
if !unit.is_build_script() {
writeln!(w, " unit{index}.name(): '{extern_crate_name}',")?
}
}
writeln!(w, " }},")?; }
writeln!(w, ")")?; }
Ok(())
}
pub fn dotify<U: Borrow<Unit>, W: io::Write>(
units: impl IntoIterator<Item = U>,
writer: W,
) -> io::Result<()> {
let mut w = writer;
writeln!(w, "digraph {{")?;
for (ix, unit) in units.into_iter().enumerate() {
let Unit {
pkg_id,
target:
Target {
name,
kind,
crate_types,
..
},
mode,
dependencies,
..
} = unit.borrow();
let pre = match pkg_id.split_once(" (") {
Some((pre, _)) => pre,
None => pkg_id,
};
let label = format!("\"{name} @ {pre}\"",);
let color = match (mode, &**kind, &**crate_types) {
(Mode::Build, [TargetKind::CustomBuild], [CrateType::Bin]) => "blue",
(Mode::RunCustomBuild, [TargetKind::CustomBuild], [CrateType::Bin]) => "yellow",
(Mode::Build, [TargetKind::Lib], [CrateType::Lib]) => "green",
(Mode::Build, [TargetKind::ProcMacro], [CrateType::ProcMacro]) => "purple",
_ => "red",
};
writeln!(w, " unit{ix} [label={label} color={color}];")?;
for Dependency { index, .. } in dependencies {
writeln!(w, " unit{index} -> unit{ix};")?
}
}
writeln!(w, "}}")?; Ok(())
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
pub struct UnitGraph {
pub units: Vec<Unit>,
pub roots: Vec<usize>,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
pub struct Unit {
pub pkg_id: String,
pub target: Target,
pub mode: Mode,
pub features: Vec<String>,
pub dependencies: Vec<Dependency>,
}
impl Unit {
pub fn is_build_script(&self) -> bool {
matches!(
(&*self.target.kind, &*self.target.crate_types),
([TargetKind::CustomBuild], [CrateType::Bin])
)
}
pub fn is_library(&self) -> bool {
matches!(
(&*self.target.kind, &*self.target.crate_types, &self.mode),
([TargetKind::Lib], [CrateType::Lib], Mode::Build)
)
}
pub fn is_proc_macro(&self) -> bool {
matches!(
(&*self.target.kind, &*self.target.crate_types, &self.mode),
([TargetKind::ProcMacro], [CrateType::ProcMacro], Mode::Build)
)
}
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
Build,
RunCustomBuild,
}
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Dependency {
pub index: usize,
pub extern_crate_name: String,
}