use anyhow::Result;
use cargo_metadata::{CargoOpt, MetadataCommand, Node, Package, Resolve};
use std::collections::{HashMap, HashSet};
use std::fmt;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone)]
pub struct ResolvedDep {
pub name: String,
pub version: String,
pub enabled_features: Vec<String>,
pub available_features: Vec<String>,
pub source: Option<String>,
pub repository: Option<String>,
pub is_direct: bool,
}
impl fmt::Display for ResolvedDep {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let tag = if self.is_direct {
"direct"
} else {
"transitive"
};
write!(f, "{} v{} ({})", self.name, self.version, tag)?;
if !self.enabled_features.is_empty() {
write!(f, " [enabled: {}]", self.enabled_features.join(", "))?;
}
if !self.available_features.is_empty() && self.available_features != self.enabled_features {
write!(f, " (available: {})", self.available_features.join(", "))?;
}
Ok(())
}
}
pub fn get_project_info(manifest_path: Option<&Path>) -> Result<(String, String)> {
let cmd = metadata_command(manifest_path);
let metadata = cmd.exec()?;
let root_id = metadata
.resolve
.as_ref()
.and_then(|r| r.root.as_ref())
.ok_or_else(|| anyhow::anyhow!("No root package found"))?;
let root_pkg = metadata
.packages
.iter()
.find(|p| &p.id == root_id)
.ok_or_else(|| anyhow::anyhow!("Root package not in packages list"))?;
Ok((root_pkg.name.to_string(), root_pkg.version.to_string()))
}
fn build_node_lookup(resolve: &Resolve) -> HashMap<String, Node> {
resolve
.nodes
.iter()
.map(|n| (n.id.to_string(), n.clone()))
.collect()
}
fn build_pkg_lookup(metadata: &cargo_metadata::Metadata) -> HashMap<String, Package> {
metadata
.packages
.iter()
.map(|p| (p.id.to_string(), p.clone()))
.collect()
}
pub fn get_deps(manifest_path: Option<&Path>) -> Result<Vec<ResolvedDep>> {
let mut cmd = metadata_command(manifest_path);
cmd.features(CargoOpt::AllFeatures);
let metadata = cmd.exec()?;
let resolve = metadata
.resolve
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No dependency resolution found"))?;
let node_map = build_node_lookup(resolve);
let pkg_map = build_pkg_lookup(&metadata);
let root_id = resolve
.root
.as_ref()
.ok_or_else(|| anyhow::anyhow!("No root package in resolve"))?;
let root_node = node_map
.get(&root_id.to_string())
.ok_or_else(|| anyhow::anyhow!("Root node not found in resolve nodes"))?;
let direct_dep_ids: HashSet<String> =
root_node.deps.iter().map(|d| d.pkg.to_string()).collect();
let mut deps = Vec::new();
for node in &resolve.nodes {
if node.id == *root_id {
continue;
}
let pkg = match pkg_map.get(&node.id.to_string()) {
Some(p) => p,
None => continue, };
let is_direct = direct_dep_ids.contains(&node.id.to_string());
let enabled_features: Vec<String> = node
.features
.iter()
.map(|s| s.as_str().to_string())
.collect();
let available_features: Vec<String> = pkg.features.keys().map(|s| s.to_string()).collect();
deps.push(ResolvedDep {
name: pkg.name.to_string(),
version: pkg.version.to_string(),
enabled_features,
available_features,
source: pkg.source.as_ref().map(|s| s.to_string()),
repository: pkg.repository.clone(),
is_direct,
});
}
Ok(deps)
}
fn metadata_command(manifest_path: Option<&Path>) -> MetadataCommand {
let mut cmd = MetadataCommand::new();
if let Some(path) = manifest_path {
cmd.manifest_path(path);
}
if lockfile_path(manifest_path).is_file() {
cmd.other_options(vec!["--locked".to_string()]);
}
cmd
}
fn lockfile_path(manifest_path: Option<&Path>) -> PathBuf {
manifest_path
.and_then(Path::parent)
.map(|path| path.join("Cargo.lock"))
.unwrap_or_else(|| PathBuf::from("Cargo.lock"))
}