1use anyhow::{bail, Result};
5use cargo_metadata::{CargoOpt, DependencyKind, MetadataCommand, Node, Package, PackageId, 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
24#[derive(Debug, Clone)]
28pub struct PackageResult {
29 pub name: String,
30 pub version: String,
31 pub manifest_path: PathBuf,
32 pub deps: Vec<ResolvedDep>,
33}
34
35impl fmt::Display for ResolvedDep {
36 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
37 let tag = if self.is_direct {
38 "direct"
39 } else {
40 "transitive"
41 };
42 write!(f, "{} v{} ({})", self.name, self.version, tag)?;
43 if !self.enabled_features.is_empty() {
44 write!(f, " [enabled: {}]", self.enabled_features.join(", "))?;
45 }
46 if !self.available_features.is_empty() && self.available_features != self.enabled_features {
47 write!(f, " (available: {})", self.available_features.join(", "))?;
48 }
49 Ok(())
50 }
51}
52
53pub fn get_project_info(manifest_path: Option<&Path>) -> Result<(String, String)> {
55 let cmd = metadata_command(manifest_path);
56 let metadata = cmd.exec()?;
57 let root_id = metadata
58 .resolve
59 .as_ref()
60 .and_then(|r| r.root.as_ref())
61 .ok_or_else(|| anyhow::anyhow!("No root package found"))?;
62 let root_pkg = metadata
63 .packages
64 .iter()
65 .find(|p| &p.id == root_id)
66 .ok_or_else(|| anyhow::anyhow!("Root package not in packages list"))?;
67 Ok((root_pkg.name.to_string(), root_pkg.version.to_string()))
68}
69
70fn build_node_lookup(resolve: &Resolve) -> HashMap<String, Node> {
72 resolve
73 .nodes
74 .iter()
75 .map(|n| (n.id.to_string(), n.clone()))
76 .collect()
77}
78
79fn build_pkg_lookup(metadata: &cargo_metadata::Metadata) -> HashMap<String, Package> {
81 metadata
82 .packages
83 .iter()
84 .map(|p| (p.id.to_string(), p.clone()))
85 .collect()
86}
87
88pub fn get_deps(manifest_path: Option<&Path>) -> Result<Vec<ResolvedDep>> {
93 let snapshots =
94 fetch_metadata_and_snapshots(manifest_path, SnapshotMode::RootOnly, false)?;
95 snapshots
96 .into_iter()
97 .next()
98 .map(|p| p.deps)
99 .ok_or_else(|| anyhow::anyhow!("No dependency snapshot produced"))
100}
101
102pub fn get_package_snapshots(
111 manifest_path: Option<&Path>,
112 workspace_all_members: bool,
113 package_filters: &[String],
114 all_targets: bool,
115) -> Result<Vec<PackageResult>> {
116 fetch_metadata_and_snapshots(
117 manifest_path,
118 SnapshotMode::Custom {
119 workspace_all_members,
120 filters: package_filters,
121 },
122 all_targets,
123 )
124}
125
126#[derive(Clone, Copy, Debug)]
127enum SnapshotMode<'a> {
128 RootOnly,
129 Custom {
130 workspace_all_members: bool,
131 filters: &'a [String],
132 },
133}
134
135fn fetch_metadata_and_snapshots(
136 manifest_path: Option<&Path>,
137 mode: SnapshotMode<'_>,
138 all_targets: bool,
139) -> Result<Vec<PackageResult>> {
140 let mut cmd = metadata_command(manifest_path);
141 cmd.features(CargoOpt::AllFeatures);
142
143 let metadata = cmd.exec()?;
144 let resolve = metadata
145 .resolve
146 .as_ref()
147 .ok_or_else(|| anyhow::anyhow!("No dependency resolution found"))?;
148
149 let node_map = build_node_lookup(resolve);
150 let pkg_map = build_pkg_lookup(&metadata);
151
152 let workspace_packages_ordered: Vec<&Package> = metadata
153 .workspace_members
154 .iter()
155 .filter_map(|id| metadata.packages.iter().find(|p| &p.id == id))
156 .collect();
157
158 let resolve_workspace_root_pkg = || -> Result<&Package> {
159 let root_id = resolve
160 .root
161 .as_ref()
162 .ok_or_else(|| anyhow::anyhow!("No root package in resolve"))?;
163 pkg_map
164 .get(&root_id.to_string())
165 .ok_or_else(|| anyhow::anyhow!("Root package not in cargo metadata packages list"))
166 };
167
168 let targets = match mode {
169 SnapshotMode::RootOnly => vec![resolve_workspace_root_pkg()?],
170 SnapshotMode::Custom {
171 workspace_all_members,
172 filters,
173 } => {
174 if workspace_all_members && filters.is_empty() {
175 workspace_packages_ordered
176 } else if !filters.is_empty() {
177 let wanted_set: HashSet<String> = filters
178 .iter()
179 .map(|s| s.trim().to_ascii_lowercase())
180 .filter(|s| !s.is_empty())
181 .collect();
182
183 let mut matched: Vec<&Package> = workspace_packages_ordered
184 .iter()
185 .copied()
186 .filter(|p| wanted_set.contains(&p.name.to_ascii_lowercase()))
187 .collect();
188
189 matched.sort_by(|a, b| a.name.cmp(&b.name));
190
191 let mut missing = Vec::new();
192 for w in &wanted_set {
193 if !matched
194 .iter()
195 .any(|p| p.name.eq_ignore_ascii_case(w.as_str()))
196 {
197 missing.push(w.clone());
198 }
199 }
200
201 if !missing.is_empty() {
202 bail!(
203 "no workspace package(s) matching {:?} — available: {}",
204 missing,
205 workspace_packages_ordered
206 .iter()
207 .map(|p| p.name.as_str())
208 .collect::<Vec<_>>()
209 .join(", ")
210 );
211 }
212
213 matched
214 } else {
215 vec![resolve_workspace_root_pkg()?]
216 }
217 }
218 };
219
220 let mut out = Vec::with_capacity(targets.len());
221 for pkg in targets {
222 let deps = resolve_deps_for_root(&pkg.id, resolve, &pkg_map, &node_map, all_targets)?;
223 out.push(PackageResult {
224 name: pkg.name.to_string(),
225 version: pkg.version.to_string(),
226 manifest_path: PathBuf::from(pkg.manifest_path.as_str()),
227 deps,
228 });
229 }
230
231 Ok(out)
232}
233
234fn resolve_deps_for_root(
239 root_pkg_id: &PackageId,
240 resolve: &Resolve,
241 pkg_map: &HashMap<String, Package>,
242 node_map: &HashMap<String, Node>,
243 all_targets: bool,
244) -> Result<Vec<ResolvedDep>> {
245 let root_node = node_map
246 .get(&root_pkg_id.to_string())
247 .ok_or_else(|| anyhow::anyhow!("Root node missing in resolve.nodes"))?;
248
249 let direct_dep_ids: HashSet<String> = root_node
250 .deps
251 .iter()
252 .filter(|d| {
253 all_targets
254 || d.dep_kinds
255 .iter()
256 .any(|k| k.kind == DependencyKind::Normal)
257 })
258 .map(|d| d.pkg.to_string())
259 .collect();
260
261 let mut deps = Vec::new();
262
263 for node in &resolve.nodes {
264 if node.id == *root_pkg_id {
265 continue;
266 }
267
268 let pkg = match pkg_map.get(&node.id.to_string()) {
269 Some(p) => p,
270 None => continue,
271 };
272
273 let is_direct = direct_dep_ids.contains(&node.id.to_string());
274
275 let enabled_features: Vec<String> = node
276 .features
277 .iter()
278 .map(|s| s.as_str().to_string())
279 .collect();
280
281 let available_features: Vec<String> = pkg.features.keys().map(|s| s.to_string()).collect();
282
283 deps.push(ResolvedDep {
284 name: pkg.name.to_string(),
285 version: pkg.version.to_string(),
286 enabled_features,
287 available_features,
288 source: pkg.source.as_ref().map(|s| s.to_string()),
289 repository: pkg.repository.clone(),
290 is_direct,
291 });
292 }
293
294 Ok(deps)
295}
296
297fn metadata_command(manifest_path: Option<&Path>) -> MetadataCommand {
298 let mut cmd = MetadataCommand::new();
299
300 if let Some(path) = manifest_path {
301 cmd.manifest_path(path);
302 }
303
304 if lockfile_path(manifest_path).is_file() {
305 cmd.other_options(vec!["--locked".to_string()]);
306 }
307
308 cmd
309}
310
311fn lockfile_path(manifest_path: Option<&Path>) -> PathBuf {
312 manifest_path
313 .and_then(Path::parent)
314 .map(|path| path.join("Cargo.lock"))
315 .unwrap_or_else(|| PathBuf::from("Cargo.lock"))
316}