1use crate::Platform;
7use crate::indexed::IndexedMetadata;
8use camino::{Utf8Path, Utf8PathBuf};
9use cargo_metadata::PackageId;
10use color_eyre::Result;
11use semver::Version;
12use serde::Serialize;
13use std::{
14 borrow::Borrow,
15 collections::{BTreeMap, BTreeSet, btree_map},
16 fmt,
17 path::Path,
18};
19
20fn shorten_path_relative_to<'a>(relative: &Utf8Path, path: &'a Utf8Path) -> &'a Utf8Path {
21 if path.starts_with(relative) {
22 path.strip_prefix(relative).expect("checked above")
23 } else {
24 path
25 }
26}
27
28#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
29enum AnyCrateIdent {
30 Local(Utf8PathBuf),
31 CratesIo(String),
32}
33
34impl AnyCrateIdent {
35 fn from_package(relative: &Utf8Path, package: &cargo_metadata::Package) -> Self {
36 if package.source.is_some() {
37 AnyCrateIdent::CratesIo(package.name.to_string())
38 } else {
39 let path = package.manifest_path.parent().expect("ends in /Cargo.toml");
40 AnyCrateIdent::Local(shorten_path_relative_to(relative, path).to_owned())
41 }
42 }
43
44 fn with_version(self, version: &Version) -> SpecificAnyCrateIdent {
45 match self {
46 AnyCrateIdent::CratesIo(name) => SpecificAnyCrateIdent::CratesIo(SpecificCrateIdent {
47 name,
48 version: version.clone(),
49 }),
50 AnyCrateIdent::Local(manifest_path) => SpecificAnyCrateIdent::Local(manifest_path),
51 }
52 }
53}
54
55#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
57pub struct SpecificCrateIdent {
58 pub name: String,
59 pub version: Version,
60}
61
62impl fmt::Debug for SpecificCrateIdent {
63 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
64 write!(f, "SpecificCrateIdent({self})")
65 }
66}
67
68impl fmt::Display for SpecificCrateIdent {
69 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
70 write!(f, "\"{} {}\"", self.name, self.version)
71 }
72}
73
74#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
79pub enum SpecificAnyCrateIdent {
80 Local(Utf8PathBuf),
81 CratesIo(SpecificCrateIdent),
82}
83
84impl fmt::Display for SpecificAnyCrateIdent {
85 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
86 match self {
87 SpecificAnyCrateIdent::Local(local) => write!(f, "{:?}", local),
88 SpecificAnyCrateIdent::CratesIo(ident) => write!(f, "{}", ident),
89 }
90 }
91}
92
93#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize)]
95pub struct DependencyKind {
96 pub run_at_build: bool,
98 pub only_debug_builds: bool,
100}
101
102impl From<cargo_metadata::DependencyKind> for DependencyKind {
103 fn from(dependency_kind: cargo_metadata::DependencyKind) -> Self {
104 match dependency_kind {
105 cargo_metadata::DependencyKind::Normal => DependencyKind::NORMAL,
106 cargo_metadata::DependencyKind::Development => DependencyKind::DEVELOPMENT,
107 cargo_metadata::DependencyKind::Build => DependencyKind::BUILD,
108 kind => panic!("Unsupported dependency kind in `cargo_metadata`: {kind:?}"),
109 }
110 }
111}
112
113impl DependencyKind {
114 pub const NORMAL: Self = DependencyKind {
116 run_at_build: false,
117 only_debug_builds: false,
118 };
119
120 pub const DEVELOPMENT: Self = DependencyKind {
122 run_at_build: false,
123 only_debug_builds: true,
124 };
125
126 pub const BUILD: Self = DependencyKind {
128 run_at_build: true,
129 only_debug_builds: false,
130 };
131
132 pub const fn then(self, next: DependencyKind) -> Self {
137 DependencyKind {
138 run_at_build: self.run_at_build || next.run_at_build,
139 only_debug_builds: self.only_debug_builds || next.only_debug_builds,
140 }
141 }
142
143 pub const fn merged_with(self, other: DependencyKind) -> Self {
148 DependencyKind {
149 run_at_build: self.run_at_build || other.run_at_build,
150 only_debug_builds: self.only_debug_builds && other.only_debug_builds,
151 }
152 }
153}
154
155impl fmt::Debug for DependencyKind {
156 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157 match (self.run_at_build, self.only_debug_builds) {
158 (false, false) => write!(f, "DependencyKind::NORMAL"),
159 (false, true) => write!(f, "DependencyKind::DEBUG"),
160 (true, false) => write!(f, "DependencyKind::BUILD"),
161 (true, true) => write!(f, "DependencyKind::DEBUG.then(DependencyKind::BUILD)"),
162 }
163 }
164}
165
166#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
170pub struct IncludedDependencyReason {
171 pub kind: DependencyKind,
173 pub root: Utf8PathBuf,
175 pub intermediate_root_dependency: Option<SpecificAnyCrateIdent>,
178 pub parent: SpecificAnyCrateIdent,
180}
181
182impl fmt::Debug for IncludedDependencyReason {
183 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
184 write!(f, "IncludedDependencyReason({self})")
185 }
186}
187
188impl fmt::Display for IncludedDependencyReason {
189 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
190 if self.root != "" {
191 write!(f, "{:?}", self.root)?;
192 }
193 if let Some(ref intermediate) = self.intermediate_root_dependency {
194 write!(f, ".{intermediate}")?;
195 if self.parent != *intermediate {
196 write!(f, "...{}", self.parent)?;
197 }
198 }
199 Ok(())
200 }
201}
202
203impl Serialize for IncludedDependencyReason {
204 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
205 self.to_string().serialize(serializer)
206 }
207}
208
209pub type Reasons = BTreeMap<IncludedDependencyReason, BTreeSet<Platform>>;
214
215pub struct IncludedDependencyVersion {
218 pub kind: DependencyKind,
219 pub has_build_rs: bool,
220 pub is_proc_macro: bool,
221 pub reasons: Reasons,
223 pub platforms: BTreeSet<Platform>,
225}
226
227pub type Included = BTreeMap<String, BTreeMap<Version, IncludedDependencyVersion>>;
230
231pub struct Resolved {
233 pub full_metadata: IndexedMetadata,
235 pub included: Included,
238 pub filtered: BTreeSet<SpecificCrateIdent>,
240}
241
242impl Resolved {
243 fn resolve_platform(metadata: &IndexedMetadata, included: &mut Included) {
246 #[derive(Clone)]
247 enum TodoFrom<'a> {
248 Workspace(&'a Utf8Path),
249 Dependency(IncludedDependencyReason),
250 }
251
252 struct Todo<'a> {
253 kind: DependencyKind,
254 incoming_edge: TodoFrom<'a>,
255 pkg: &'a PackageId,
256 }
257
258 let mut todos = metadata
259 .get_workspace_default_members()
260 .iter()
261 .map(|pkg| {
262 let path = shorten_path_relative_to(
263 &metadata.workspace_root,
264 &metadata.packages[pkg].manifest_path,
265 );
266 Todo {
267 kind: DependencyKind::NORMAL,
268 incoming_edge: TodoFrom::Workspace(path),
269 pkg,
270 }
271 })
272 .collect::<Vec<_>>();
273
274 while let Some(todo) = todos.pop() {
275 let package = &metadata.packages[todo.pkg];
276 let node = &metadata.resolve[todo.pkg];
277
278 let package_ident = AnyCrateIdent::from_package(&metadata.workspace_root, package);
279
280 let mut has_build_rs = false;
281 let mut is_proc_macro = false;
282 for target in package.targets.iter().flat_map(|target| &target.kind) {
283 use cargo_metadata::TargetKind;
284
285 match target {
286 TargetKind::Bench
287 | TargetKind::Bin
288 | TargetKind::CDyLib
289 | TargetKind::DyLib
290 | TargetKind::Example
291 | TargetKind::Lib
292 | TargetKind::RLib
293 | TargetKind::StaticLib
294 | TargetKind::Test => (),
295 TargetKind::CustomBuild => has_build_rs = true,
296 TargetKind::ProcMacro => is_proc_macro = true,
297 _ => panic!("Unknown target kind"),
298 }
299 }
300
301 let mut package_kind = todo.kind;
302 if is_proc_macro {
303 package_kind.run_at_build = true;
304 }
305
306 if let AnyCrateIdent::CratesIo(ref name) = package_ident {
307 let version = included
308 .entry(name.clone())
309 .or_default()
310 .entry(package.version.clone());
311 let inserted_new = matches!(version, btree_map::Entry::Vacant(_));
312 let version = version.or_insert_with(|| IncludedDependencyVersion {
313 kind: package_kind,
314 has_build_rs,
315 is_proc_macro,
316 reasons: BTreeMap::new(),
317 platforms: BTreeSet::new(),
318 });
319
320 let package_kind = version.kind.merged_with(package_kind);
321 let new_kind = package_kind != version.kind;
322 version.kind = package_kind;
323
324 match todo.incoming_edge {
327 TodoFrom::Workspace(_) => (),
328 TodoFrom::Dependency(ref reason) => {
329 let entry = version.reasons.entry(reason.clone()).or_default(); if let Some(platform) = metadata.platform.clone() {
331 entry.insert(platform);
332 }
333 }
334 };
335
336 let new_platform = metadata
337 .platform
338 .clone()
339 .is_some_and(|platform| version.platforms.insert(platform));
340
341 if !(inserted_new || new_kind || new_platform) {
342 continue;
343 }
344 }
345
346 let dep_parent = package_ident.with_version(&package.version);
347
348 todos.extend(node.deps.iter().filter_map(|dep| {
349 let dep_kind = dep
350 .dep_kinds
351 .iter()
352 .filter(|kind| {
353 matches!(todo.incoming_edge, TodoFrom::Workspace(_))
355 || kind.kind != cargo_metadata::DependencyKind::Development
356 })
357 .map(|kind| package_kind.then(kind.kind.into()))
358 .reduce(DependencyKind::merged_with)?;
359
360 let (root, intermediate_root_dependency) = match todo.incoming_edge {
361 TodoFrom::Workspace(root) => (root.to_owned(), None),
362 TodoFrom::Dependency(ref reason) => {
363 let intermediate_root_dependency = reason
364 .intermediate_root_dependency
365 .clone()
366 .unwrap_or_else(|| dep_parent.clone());
367
368 (reason.root.clone(), Some(intermediate_root_dependency))
369 }
370 };
371
372 Some(Todo {
373 kind: dep_kind,
374 incoming_edge: TodoFrom::Dependency(IncludedDependencyReason {
375 kind: package_kind,
376 root,
377 intermediate_root_dependency,
378 parent: dep_parent.clone(),
379 }),
380 pkg: &dep.pkg,
381 })
382 }));
383 }
384 }
385
386 pub fn resolve_from_indexed(
388 included: impl IntoIterator<Item: Borrow<IndexedMetadata>>,
389 ) -> Included {
390 let mut out = Included::new();
391 for included in included {
392 Self::resolve_platform(included.borrow(), &mut out);
393 }
394 out
395 }
396
397 pub fn resolve_filtered_from_indexed(
400 included: Included,
401 full_metadata: IndexedMetadata,
402 ) -> Self {
403 assert_eq!(full_metadata.platform, None);
404
405 let mut filtered = BTreeSet::new();
406
407 for pkg in full_metadata.packages.values() {
408 if let AnyCrateIdent::CratesIo(name) =
409 AnyCrateIdent::from_package(&full_metadata.workspace_root, pkg)
410 {
411 let was_included = included
412 .get(&name)
413 .is_some_and(|versions| versions.contains_key(&pkg.version));
414 if !was_included {
415 filtered.insert(SpecificCrateIdent {
416 name,
417 version: pkg.version.clone(),
418 });
419 }
420 }
421 }
422
423 Resolved {
424 full_metadata,
425 included,
426 filtered,
427 }
428 }
429
430 pub fn resolve_from_path(
432 root_cargo_toml: &Path,
433 specific_platforms: impl IntoIterator<Item = Platform>,
434 include_all_platforms: bool,
435 ) -> Result<Self> {
436 let mut included = itertools::process_results(
437 specific_platforms
438 .into_iter()
439 .map(|platform| IndexedMetadata::gather(root_cargo_toml, Some(platform))),
440 |iter| Self::resolve_from_indexed(iter),
441 )?;
442
443 let full_metadata = IndexedMetadata::gather(root_cargo_toml, None)?;
444 let out = if include_all_platforms {
445 Self::resolve_platform(&full_metadata, &mut included);
446 Resolved {
447 full_metadata,
448 included,
449 filtered: BTreeSet::new(),
450 }
451 } else {
452 Self::resolve_filtered_from_indexed(included, full_metadata)
453 };
454
455 Ok(out)
456 }
457}