use super::Host;
use super::common::{Common, CommonArgs};
use crate::Result;
use crate::facts::CrateRef;
use cargo_metadata::{CargoOpt, DependencyKind, Node, Package, PackageId};
use clap::{Parser, ValueEnum};
use ohno::{IntoAppError, bail};
use serde::{Deserialize, Serialize};
use std::collections::{HashMap, HashSet};
use strum::{Display, EnumString};
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, ValueEnum, Deserialize, Serialize, Display, EnumString)]
#[value(rename_all = "lowercase")]
#[strum(serialize_all = "lowercase")]
pub enum DependencyType {
Standard,
Dev,
Build,
}
#[derive(Parser, Debug)]
pub struct DepsArgs {
#[arg(
long = "dependency-types",
value_delimiter = ',',
value_name = "TYPES",
default_value = "standard,dev,build"
)]
pub dependency_types: Option<Vec<DependencyType>>,
#[arg(short = 'F', long, value_name = "FEATURES", help_heading = "Feature Selection")]
pub features: Vec<String>,
#[arg(long, help_heading = "Feature Selection")]
pub all_features: bool,
#[arg(long, help_heading = "Feature Selection")]
pub no_default_features: bool,
#[arg(short = 'p', long, value_name = "SPEC", help_heading = "Package Selection")]
pub package: Vec<String>,
#[arg(long, help_heading = "Package Selection")]
pub workspace: bool,
#[command(flatten)]
pub common: CommonArgs,
}
pub async fn process_dependencies<H: Host>(host: &mut H, args: &DepsArgs) -> Result<()> {
let mut common = Common::new(host, &args.common).await?;
if args.all_features {
_ = common.metadata_cmd.features(CargoOpt::AllFeatures);
} else {
if args.no_default_features {
_ = common.metadata_cmd.features(CargoOpt::NoDefaultFeatures);
}
if !args.features.is_empty() {
_ = common.metadata_cmd.features(CargoOpt::SomeFeatures(args.features.clone()));
}
}
let metadata = common.metadata_cmd.exec().into_app_err("unable to retrieve workspace metadata")?;
let all_packages: HashMap<_, _> = metadata.packages.iter().map(|p| (&p.id, p)).collect();
let resolve_index: HashMap<&PackageId, &Node> = metadata
.resolve
.as_ref()
.map_or_else(HashMap::default, |r| r.nodes.iter().map(|n| (&n.id, n)).collect());
if !args.package.is_empty() {
let workspace_packages: Vec<_> = metadata
.workspace_members
.iter()
.filter_map(|id| all_packages.get(id).map(|p| &p.name))
.collect();
for pkg_name in &args.package {
if !workspace_packages.iter().any(|&name| name == pkg_name) {
bail!("package '{pkg_name}' not found in workspace");
}
}
}
if !args.package.is_empty() {
process_packages(
args,
&mut common,
&all_packages,
&resolve_index,
metadata
.workspace_members
.iter()
.filter_map(|id| all_packages.get(id).copied())
.filter(|p| args.package.contains(&p.name)),
)
.await
} else if args.workspace {
process_packages(
args,
&mut common,
&all_packages,
&resolve_index,
metadata.workspace_members.iter().filter_map(|id| all_packages.get(id).copied()),
)
.await
} else if let Some(root) = metadata.root_package() {
process_packages(args, &mut common, &all_packages, &resolve_index, core::iter::once(root)).await
} else {
process_packages(
args,
&mut common,
&all_packages,
&resolve_index,
metadata.workspace_members.iter().filter_map(|id| all_packages.get(id).copied()),
)
.await
}
}
async fn process_packages<'a, H: Host>(
args: &DepsArgs,
common: &mut Common<'_, H>,
all_packages: &HashMap<&'a PackageId, &'a Package>,
resolve_index: &HashMap<&'a PackageId, &'a Node>,
target_packages: impl Iterator<Item = &'a Package>,
) -> Result<()> {
let should_process_std = args
.dependency_types
.as_ref()
.is_none_or(|d| d.is_empty() || d.contains(&DependencyType::Standard));
let should_process_dev = args
.dependency_types
.as_ref()
.is_none_or(|d| d.is_empty() || d.contains(&DependencyType::Dev));
let should_process_build = args
.dependency_types
.as_ref()
.is_none_or(|d| d.is_empty() || d.contains(&DependencyType::Build));
let mut crate_dep_pairs: Vec<(CrateRef, DependencyType)> = Vec::new();
for package in target_packages {
if should_process_std {
crate_dep_pairs.extend(build_transitive_deps(
all_packages,
resolve_index,
&package.id,
DependencyType::Standard,
));
}
if should_process_dev {
crate_dep_pairs.extend(build_transitive_deps(all_packages, resolve_index, &package.id, DependencyType::Dev));
}
if should_process_build {
crate_dep_pairs.extend(build_transitive_deps(
all_packages,
resolve_index,
&package.id,
DependencyType::Build,
));
}
}
let facts = common
.process_crates(crate_dep_pairs.iter().map(|(crate_ref, _)| crate_ref.clone()), false)
.await?;
common.report(facts.into_iter())
}
fn build_transitive_deps<'a>(
all_packages: &HashMap<&'a PackageId, &'a Package>,
resolve_index: &HashMap<&'a PackageId, &'a Node>,
target_package_id: &PackageId,
dependency_type: DependencyType,
) -> HashSet<(CrateRef, DependencyType)> {
let initial_kind = match dependency_type {
DependencyType::Standard => DependencyKind::Normal,
DependencyType::Dev => DependencyKind::Development,
DependencyType::Build => DependencyKind::Build,
};
let mut result = HashSet::new();
let mut visited: HashSet<&PackageId> = HashSet::new();
let mut queue: Vec<&PackageId> = Vec::new();
if let Some(node) = resolve_index.get(target_package_id) {
for dep in &node.deps {
if dep.dep_kinds.iter().any(|dk| dk.kind == initial_kind) {
queue.push(&dep.pkg);
}
}
}
while let Some(pkg_id) = queue.pop() {
if !visited.insert(pkg_id) {
continue;
}
if let Some(pkg) = all_packages.get(pkg_id) {
_ = result.insert((CrateRef::new(&pkg.name, Some(pkg.version.clone())), dependency_type));
if let Some(node) = resolve_index.get(pkg_id) {
for dep in &node.deps {
if dep.dep_kinds.iter().any(|dk| dk.kind == DependencyKind::Normal) {
queue.push(&dep.pkg);
}
}
}
}
}
result
}