cargo2meson 0.1.1

generate traits from C header files
Documentation
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;

/// Remove all build scripts and their ancestors,
/// emitting a meson build script to the given writer.
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, "    ],")?; // rust_args
        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, "    ],")?; // link_with

            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, "    }},")?; // rust_dependency_map
        }
        writeln!(w, ")")?; // unit
    }
    Ok(())
}

/// Emit the build graph in graphviz format.
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, "}}")?; // digraph
    Ok(())
}

/// Simplified root of the result of `cargo +nightly build -Zunstable-options --unit-graph`
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, Hash)]
pub struct UnitGraph {
    pub units: Vec<Unit>,
    pub roots: Vec<usize>,
}

/// See [`UnitGraph`].
#[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)
        )
    }
}

/// See [`Unit`].
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
    Build,
    RunCustomBuild,
}

/// See [`Unit`].
#[derive(Debug, Clone, Deserialize, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Dependency {
    pub index: usize,
    pub extern_crate_name: String,
}