1use anyhow::Result;
5use cargo_metadata::{CargoOpt, MetadataCommand, Node, Package, Resolve};
6use std::collections::{HashMap, HashSet};
7use std::fmt;
8use std::path::{Path, PathBuf};
9
10#[derive(Debug, Clone)]
12pub struct ResolvedDep {
13 pub name: String,
14 pub version: String,
15 pub enabled_features: Vec<String>,
17 pub available_features: Vec<String>,
19 pub source: Option<String>,
20 pub repository: Option<String>,
21 pub is_direct: bool,
22}
23
24impl fmt::Display for ResolvedDep {
25 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
26 let tag = if self.is_direct {
27 "direct"
28 } else {
29 "transitive"
30 };
31 write!(f, "{} v{} ({})", self.name, self.version, tag)?;
32 if !self.enabled_features.is_empty() {
33 write!(f, " [enabled: {}]", self.enabled_features.join(", "))?;
34 }
35 if !self.available_features.is_empty() && self.available_features != self.enabled_features {
36 write!(f, " (available: {})", self.available_features.join(", "))?;
37 }
38 Ok(())
39 }
40}
41
42pub fn get_project_info(manifest_path: Option<&Path>) -> Result<(String, String)> {
44 let cmd = metadata_command(manifest_path);
45 let metadata = cmd.exec()?;
46 let root_id = metadata
47 .resolve
48 .as_ref()
49 .and_then(|r| r.root.as_ref())
50 .ok_or_else(|| anyhow::anyhow!("No root package found"))?;
51 let root_pkg = metadata
52 .packages
53 .iter()
54 .find(|p| &p.id == root_id)
55 .ok_or_else(|| anyhow::anyhow!("Root package not in packages list"))?;
56 Ok((root_pkg.name.to_string(), root_pkg.version.to_string()))
57}
58
59fn build_node_lookup(resolve: &Resolve) -> HashMap<String, Node> {
61 resolve
62 .nodes
63 .iter()
64 .map(|n| (n.id.to_string(), n.clone()))
65 .collect()
66}
67
68fn build_pkg_lookup(metadata: &cargo_metadata::Metadata) -> HashMap<String, Package> {
70 metadata
71 .packages
72 .iter()
73 .map(|p| (p.id.to_string(), p.clone()))
74 .collect()
75}
76
77pub fn get_deps(manifest_path: Option<&Path>) -> Result<Vec<ResolvedDep>> {
82 let mut cmd = metadata_command(manifest_path);
83 cmd.features(CargoOpt::AllFeatures);
84
85 let metadata = cmd.exec()?;
86 let resolve = metadata
87 .resolve
88 .as_ref()
89 .ok_or_else(|| anyhow::anyhow!("No dependency resolution found"))?;
90
91 let node_map = build_node_lookup(resolve);
93 let pkg_map = build_pkg_lookup(&metadata);
94
95 let root_id = resolve
97 .root
98 .as_ref()
99 .ok_or_else(|| anyhow::anyhow!("No root package in resolve"))?;
100
101 let root_node = node_map
103 .get(&root_id.to_string())
104 .ok_or_else(|| anyhow::anyhow!("Root node not found in resolve nodes"))?;
105
106 let direct_dep_ids: HashSet<String> =
108 root_node.deps.iter().map(|d| d.pkg.to_string()).collect();
109
110 let mut deps = Vec::new();
111
112 for node in &resolve.nodes {
113 if node.id == *root_id {
115 continue;
116 }
117
118 let pkg = match pkg_map.get(&node.id.to_string()) {
119 Some(p) => p,
120 None => continue, };
122
123 let is_direct = direct_dep_ids.contains(&node.id.to_string());
124
125 let enabled_features: Vec<String> = node
127 .features
128 .iter()
129 .map(|s| s.as_str().to_string())
130 .collect();
131
132 let available_features: Vec<String> = pkg.features.keys().map(|s| s.to_string()).collect();
134
135 deps.push(ResolvedDep {
136 name: pkg.name.to_string(),
137 version: pkg.version.to_string(),
138 enabled_features,
139 available_features,
140 source: pkg.source.as_ref().map(|s| s.to_string()),
141 repository: pkg.repository.clone(),
142 is_direct,
143 });
144 }
145
146 Ok(deps)
147}
148
149fn metadata_command(manifest_path: Option<&Path>) -> MetadataCommand {
150 let mut cmd = MetadataCommand::new();
151
152 if let Some(path) = manifest_path {
153 cmd.manifest_path(path);
154 }
155
156 if lockfile_path(manifest_path).is_file() {
157 cmd.other_options(vec!["--locked".to_string()]);
158 }
159
160 cmd
161}
162
163fn lockfile_path(manifest_path: Option<&Path>) -> PathBuf {
164 manifest_path
165 .and_then(Path::parent)
166 .map(|path| path.join("Cargo.lock"))
167 .unwrap_or_else(|| PathBuf::from("Cargo.lock"))
168}