1 //! Gathering dependencies is the largest part of annotating.
2
3 use std::collections::{BTreeMap, BTreeSet};
4
5 use anyhow::{bail, Result};
6 use cargo_metadata::{
7 DependencyKind, Metadata as CargoMetadata, Node, NodeDep, Package, PackageId, Target,
8 };
9 use cargo_platform::Platform;
10 use serde::{Deserialize, Serialize};
11
12 use crate::metadata::{CrateId, TreeResolverMetadata};
13 use crate::select::Select;
14 use crate::utils::sanitize_module_name;
15
16 /// A representation of a crate dependency
17 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
18 pub(crate) struct Dependency {
19 /// The PackageId of the target
20 pub(crate) package_id: PackageId,
21
22 /// The library target name of the dependency.
23 pub(crate) target_name: String,
24
25 /// The alias for the dependency from the perspective of the current package
26 pub(crate) alias: Option<String>,
27 }
28
29 /// A collection of [Dependency]s sorted by dependency kind.
30 #[derive(Debug, Default, Serialize, Deserialize)]
31 pub(crate) struct DependencySet {
32 pub(crate) normal_deps: Select<BTreeSet<Dependency>>,
33 pub(crate) normal_dev_deps: Select<BTreeSet<Dependency>>,
34 pub(crate) proc_macro_deps: Select<BTreeSet<Dependency>>,
35 pub(crate) proc_macro_dev_deps: Select<BTreeSet<Dependency>>,
36 pub(crate) build_deps: Select<BTreeSet<Dependency>>,
37 pub(crate) build_link_deps: Select<BTreeSet<Dependency>>,
38 pub(crate) build_proc_macro_deps: Select<BTreeSet<Dependency>>,
39 }
40
41 impl DependencySet {
42 /// Collect all dependencies for a given node in the resolve graph.
new_for_node( node: &Node, metadata: &CargoMetadata, resolver_data: &TreeResolverMetadata, ) -> Self43 pub(crate) fn new_for_node(
44 node: &Node,
45 metadata: &CargoMetadata,
46 resolver_data: &TreeResolverMetadata,
47 ) -> Self {
48 // Build a dep tree mapping that's easily indexable via `cargo_metadata::PackageId`
49 let dep_tree: BTreeMap<CrateId, Select<BTreeSet<CrateId>>> = resolver_data
50 .iter()
51 .map(|(id, tree_data)| {
52 let mut select = Select::new();
53 for (config, data) in tree_data.items() {
54 for dep in data.deps {
55 select.insert(dep, config.clone());
56 }
57 }
58 (id.clone(), select)
59 })
60 .collect();
61
62 let crate_id = {
63 let package = &metadata[&node.id];
64 CrateId::from(package)
65 };
66
67 let (normal_dev_deps, normal_deps) = {
68 let (dev, normal) = node
69 .deps
70 .iter()
71 // Do not track workspace members as dependencies. Users are expected to maintain those connections
72 .filter(|dep| !is_workspace_member(dep, metadata))
73 .filter(|dep| is_lib_package(&metadata[&dep.pkg]))
74 .filter(|dep| is_normal_dependency(dep) || is_dev_dependency(dep))
75 .partition(|dep| is_dev_dependency(dep));
76
77 (
78 collect_deps_selectable(
79 node,
80 dev,
81 metadata,
82 DependencyKind::Development,
83 dep_tree.get(&crate_id),
84 ),
85 collect_deps_selectable(
86 node,
87 normal,
88 metadata,
89 DependencyKind::Normal,
90 dep_tree.get(&crate_id),
91 ),
92 )
93 };
94
95 let (proc_macro_dev_deps, proc_macro_deps) = {
96 let (dev, normal) = node
97 .deps
98 .iter()
99 // Do not track workspace members as dependencies. Users are expected to maintain those connections
100 .filter(|dep| !is_workspace_member(dep, metadata))
101 .filter(|dep| is_proc_macro_package(&metadata[&dep.pkg]))
102 .filter(|dep| is_normal_dependency(dep) || is_dev_dependency(dep))
103 .partition(|dep| is_dev_dependency(dep));
104
105 (
106 collect_deps_selectable(
107 node,
108 dev,
109 metadata,
110 DependencyKind::Development,
111 dep_tree.get(&crate_id),
112 ),
113 collect_deps_selectable(
114 node,
115 normal,
116 metadata,
117 DependencyKind::Normal,
118 dep_tree.get(&crate_id),
119 ),
120 )
121 };
122
123 // For rules on build script dependencies see:
124 // https://doc.rust-lang.org/cargo/reference/build-scripts.html#build-dependencies
125 let (build_proc_macro_deps, build_deps) = {
126 let (proc_macro, normal) = node
127 .deps
128 .iter()
129 // Do not track workspace members as dependencies. Users are expected to maintain those connections
130 .filter(|dep| !is_workspace_member(dep, metadata))
131 .filter(|dep| is_build_dependency(dep))
132 .filter(|dep| !is_dev_dependency(dep))
133 .partition(|dep| is_proc_macro_package(&metadata[&dep.pkg]));
134
135 (
136 collect_deps_selectable(
137 node,
138 proc_macro,
139 metadata,
140 DependencyKind::Build,
141 dep_tree.get(&crate_id),
142 ),
143 collect_deps_selectable(
144 node,
145 normal,
146 metadata,
147 DependencyKind::Build,
148 dep_tree.get(&crate_id),
149 ),
150 )
151 };
152
153 // packages with the `links` property follow slightly different rules than other
154 // dependencies. These packages provide zero or more environment variables to the build
155 // script's of packages that directly (non-transitively) depend on these packages. Note that
156 // dependency specifically means of the package (`dependencies`), and not of the build
157 // script (`build-dependencies`).
158 // https://doc.rust-lang.org/cargo/reference/build-scripts.html#the-links-manifest-key
159 // https://doc.rust-lang.org/cargo/reference/build-scripts.html#-sys-packages
160 // https://doc.rust-lang.org/cargo/reference/build-script-examples.html#using-another-sys-crate
161 let mut build_link_deps: Select<BTreeSet<Dependency>> = Select::default();
162 for (configuration, dependency) in normal_deps
163 .items()
164 .into_iter()
165 .filter(|(_, dependency)| metadata[&dependency.package_id].links.is_some())
166 {
167 // Add any normal dependency to build dependencies that are associated `*-sys` crates
168 build_link_deps.insert(dependency.clone(), configuration.clone());
169 }
170
171 Self {
172 normal_deps,
173 normal_dev_deps,
174 proc_macro_deps,
175 proc_macro_dev_deps,
176 build_deps,
177 build_link_deps,
178 build_proc_macro_deps,
179 }
180 }
181 }
182
183 /// For details on optional dependencies see [the Rust docs](https://doc.rust-lang.org/cargo/reference/features.html#optional-dependencies).
is_optional_dependency( parent: &Node, dep: &NodeDep, target: Option<&Platform>, metadata: &CargoMetadata, kind: DependencyKind, ) -> bool184 fn is_optional_dependency(
185 parent: &Node,
186 dep: &NodeDep,
187 target: Option<&Platform>,
188 metadata: &CargoMetadata,
189 kind: DependencyKind,
190 ) -> bool {
191 let pkg = &metadata[&parent.id];
192
193 pkg.dependencies
194 .iter()
195 .filter(|&d| d.kind == kind)
196 .filter(|&d| d.target.as_ref() == target)
197 .filter(|&d| d.optional)
198 .any(|d| sanitize_module_name(d.rename.as_ref().unwrap_or(&d.name)) == dep.name)
199 }
200
collect_deps_selectable( node: &Node, deps: Vec<&NodeDep>, metadata: &cargo_metadata::Metadata, kind: DependencyKind, tree_data: Option<&Select<BTreeSet<CrateId>>>, ) -> Select<BTreeSet<Dependency>>201 fn collect_deps_selectable(
202 node: &Node,
203 deps: Vec<&NodeDep>,
204 metadata: &cargo_metadata::Metadata,
205 kind: DependencyKind,
206 tree_data: Option<&Select<BTreeSet<CrateId>>>,
207 ) -> Select<BTreeSet<Dependency>> {
208 let mut select: Select<BTreeSet<Dependency>> = Select::default();
209
210 // Unfortunately, Cargo metadata is not as accurate as it could be due
211 // to the lack of `resolver = 2` support in the `cargo metadata` subcommand.
212 // To ensure accurate dependencies are determined, metadata is only used to
213 // determine the general list of dependencies but any one of them can be demoted
214 // to a platform specific dep depending on the `cargo tree` data provided.
215 //
216 // For more details see: https://github.com/rust-lang/cargo/issues/9863
217 for dep in deps.into_iter() {
218 let dep_pkg = &metadata[&dep.pkg];
219 let target_name = get_library_target_name(dep_pkg, &dep.name)
220 .expect("Nodes Dependencies are expected to exclusively be library-like targets");
221 let alias = get_target_alias(&dep.name, dep_pkg);
222 let crate_id = CrateId::from(dep_pkg);
223
224 for kind_info in &dep.dep_kinds {
225 if kind_info.kind != kind {
226 continue;
227 }
228
229 // For optional dependencies, use the Feature tree resolver data to determine
230 // if or how the dependency should be added.
231 if is_optional_dependency(node, dep, kind_info.target.as_ref(), metadata, kind) {
232 // Collect the dependency from any configuration.
233 if let Some(data) = tree_data {
234 for (config, tree_dep) in data.items() {
235 if crate_id == tree_dep {
236 let dependency = Dependency {
237 package_id: dep.pkg.clone(),
238 target_name: target_name.clone(),
239 alias: alias.clone(),
240 };
241 select.insert(dependency, config);
242 }
243 }
244 }
245 } else {
246 let dependency = Dependency {
247 package_id: dep.pkg.clone(),
248 target_name: target_name.clone(),
249 alias: alias.clone(),
250 };
251 select.insert(
252 dependency,
253 kind_info
254 .target
255 .as_ref()
256 .map(|platform| platform.to_string()),
257 );
258 }
259 }
260 }
261
262 select
263 }
264
265 /// Packages may have targets that match aliases of dependents. This function
266 /// checks a target to see if it's an unexpected type for a dependency.
is_ignored_package_target(target: &Target) -> bool267 fn is_ignored_package_target(target: &Target) -> bool {
268 target
269 .kind
270 .iter()
271 .any(|t| ["example", "bench", "test"].contains(&t.as_str()))
272 }
273
is_lib_package(package: &Package) -> bool274 fn is_lib_package(package: &Package) -> bool {
275 package.targets.iter().any(|target| {
276 target
277 .crate_types
278 .iter()
279 .any(|t| ["lib", "rlib"].contains(&t.as_str()))
280 && !is_ignored_package_target(target)
281 })
282 }
283
is_proc_macro_package(package: &Package) -> bool284 fn is_proc_macro_package(package: &Package) -> bool {
285 package.targets.iter().any(|target| {
286 target.crate_types.iter().any(|t| t == "proc-macro") && !is_ignored_package_target(target)
287 })
288 }
289
is_dev_dependency(node_dep: &NodeDep) -> bool290 fn is_dev_dependency(node_dep: &NodeDep) -> bool {
291 let is_normal_dep = is_normal_dependency(node_dep);
292 let is_dev_dep = node_dep
293 .dep_kinds
294 .iter()
295 .any(|k| matches!(k.kind, cargo_metadata::DependencyKind::Development));
296
297 // In the event that a dependency is listed as both a dev and normal dependency,
298 // it's only considered a dev dependency if it's __not__ a normal dependency.
299 !is_normal_dep && is_dev_dep
300 }
301
is_build_dependency(node_dep: &NodeDep) -> bool302 fn is_build_dependency(node_dep: &NodeDep) -> bool {
303 node_dep
304 .dep_kinds
305 .iter()
306 .any(|k| matches!(k.kind, cargo_metadata::DependencyKind::Build))
307 }
308
is_normal_dependency(node_dep: &NodeDep) -> bool309 fn is_normal_dependency(node_dep: &NodeDep) -> bool {
310 node_dep
311 .dep_kinds
312 .iter()
313 .any(|k| matches!(k.kind, cargo_metadata::DependencyKind::Normal))
314 }
315
is_workspace_member(node_dep: &NodeDep, metadata: &CargoMetadata) -> bool316 fn is_workspace_member(node_dep: &NodeDep, metadata: &CargoMetadata) -> bool {
317 metadata
318 .workspace_members
319 .iter()
320 .any(|id| id == &node_dep.pkg)
321 }
322
get_library_target_name(package: &Package, potential_name: &str) -> Result<String>323 fn get_library_target_name(package: &Package, potential_name: &str) -> Result<String> {
324 // If the potential name is not an alias in a dependent's package, a target's name
325 // should match which means we already know what the target library name is. The
326 // only exception is for targets that are otherwise ignored (like benchmarks or examples).
327 if package
328 .targets
329 .iter()
330 .any(|t| t.name == potential_name && !is_ignored_package_target(t))
331 {
332 return Ok(potential_name.to_string());
333 }
334
335 // Locate any library type targets
336 let lib_targets: Vec<&cargo_metadata::Target> = package
337 .targets
338 .iter()
339 .filter(|t| {
340 t.kind
341 .iter()
342 .any(|k| k == "lib" || k == "rlib" || k == "proc-macro")
343 })
344 .collect();
345
346 // Only one target should be found
347 if lib_targets.len() != 1 {
348 bail!(
349 "Unexpected number of 'library-like' targets found for {}: {:?}",
350 package.name,
351 package.targets
352 )
353 }
354
355 let target = lib_targets.into_iter().last().unwrap();
356 Ok(target.name.clone())
357 }
358
359 /// The resolve graph (resolve.nodes[#].deps[#].name) of Cargo metadata uses module names
360 /// for targets where packages (packages[#].targets[#].name) uses crate names. In order to
361 /// determine whether or not a dependency is aliased, we compare it with all available targets
362 /// on it's package. Note that target names are not guaranteed to be module names where Node
363 /// dependencies are, so we need to do a conversion to check for this. This function will
364 /// return the name of a target's alias in the content of the current dependent if it is aliased.
get_target_alias(target_name: &str, package: &Package) -> Option<String>365 fn get_target_alias(target_name: &str, package: &Package) -> Option<String> {
366 match package
367 .targets
368 .iter()
369 .filter(|t| !is_ignored_package_target(t))
370 .all(|t| sanitize_module_name(&t.name) != target_name)
371 {
372 true => Some(target_name.to_string()),
373 false => None,
374 }
375 }
376
377 #[cfg(test)]
378 mod test {
379 use super::*;
380
381 use semver::Version;
382
383 use crate::metadata::CargoTreeEntry;
384 use crate::test::*;
385
386 #[test]
get_expected_lib_target_name()387 fn get_expected_lib_target_name() {
388 let mut package = mock_cargo_metadata_package();
389 package
390 .targets
391 .extend(vec![serde_json::from_value(serde_json::json!({
392 "name": "potential",
393 "kind": ["lib"],
394 "crate_types": [],
395 "required_features": [],
396 "src_path": "/tmp/mock.rs",
397 "edition": "2021",
398 "doctest": false,
399 "test": false,
400 "doc": false,
401 }))
402 .unwrap()]);
403
404 assert_eq!(
405 get_library_target_name(&package, "potential").unwrap(),
406 "potential"
407 );
408 }
409
410 #[test]
get_lib_target_name()411 fn get_lib_target_name() {
412 let mut package = mock_cargo_metadata_package();
413 package
414 .targets
415 .extend(vec![serde_json::from_value(serde_json::json!({
416 "name": "lib_target",
417 "kind": ["lib"],
418 "crate_types": [],
419 "required_features": [],
420 "src_path": "/tmp/mock.rs",
421 "edition": "2021",
422 "doctest": false,
423 "test": false,
424 "doc": false,
425 }))
426 .unwrap()]);
427
428 assert_eq!(
429 get_library_target_name(&package, "mock-pkg").unwrap(),
430 "lib_target"
431 );
432 }
433
434 #[test]
get_rlib_target_name()435 fn get_rlib_target_name() {
436 let mut package = mock_cargo_metadata_package();
437 package
438 .targets
439 .extend(vec![serde_json::from_value(serde_json::json!({
440 "name": "rlib_target",
441 "kind": ["rlib"],
442 "crate_types": [],
443 "required_features": [],
444 "src_path": "/tmp/mock.rs",
445 "edition": "2021",
446 "doctest": false,
447 "test": false,
448 "doc": false,
449 }))
450 .unwrap()]);
451
452 assert_eq!(
453 get_library_target_name(&package, "mock-pkg").unwrap(),
454 "rlib_target"
455 );
456 }
457
458 #[test]
get_proc_macro_target_name()459 fn get_proc_macro_target_name() {
460 let mut package = mock_cargo_metadata_package();
461 package
462 .targets
463 .extend(vec![serde_json::from_value(serde_json::json!({
464 "name": "proc_macro_target",
465 "kind": ["proc-macro"],
466 "crate_types": [],
467 "required_features": [],
468 "src_path": "/tmp/mock.rs",
469 "edition": "2021",
470 "doctest": false,
471 "test": false,
472 "doc": false,
473 }))
474 .unwrap()]);
475
476 assert_eq!(
477 get_library_target_name(&package, "mock-pkg").unwrap(),
478 "proc_macro_target"
479 );
480 }
481
482 #[test]
get_bin_target_name()483 fn get_bin_target_name() {
484 let mut package = mock_cargo_metadata_package();
485 package
486 .targets
487 .extend(vec![serde_json::from_value(serde_json::json!({
488 "name": "bin_target",
489 "kind": ["bin"],
490 "crate_types": [],
491 "required_features": [],
492 "src_path": "/tmp/mock.rs",
493 "edition": "2021",
494 "doctest": false,
495 "test": false,
496 "doc": false,
497 }))
498 .unwrap()]);
499
500 // It's an error for no library target to be found.
501 assert!(get_library_target_name(&package, "mock-pkg").is_err());
502 }
503
504 /// Locate the [cargo_metadata::Node] for the crate matching the given name
find_metadata_node<'a>( name: &str, metadata: &'a cargo_metadata::Metadata, ) -> &'a cargo_metadata::Node505 fn find_metadata_node<'a>(
506 name: &str,
507 metadata: &'a cargo_metadata::Metadata,
508 ) -> &'a cargo_metadata::Node {
509 metadata
510 .resolve
511 .as_ref()
512 .unwrap()
513 .nodes
514 .iter()
515 .find(|node| {
516 let pkg = &metadata[&node.id];
517 pkg.name == name
518 })
519 .unwrap()
520 }
521
522 #[test]
example_proc_macro_dep()523 fn example_proc_macro_dep() {
524 let metadata = metadata::example_proc_macro_dep();
525
526 let node = find_metadata_node("example-proc-macro-dep", &metadata);
527 let dependencies =
528 DependencySet::new_for_node(node, &metadata, &TreeResolverMetadata::default());
529
530 let normal_deps: Vec<_> = dependencies
531 .normal_deps
532 .items()
533 .into_iter()
534 .map(|(_, dep)| dep.target_name)
535 .collect();
536 assert_eq!(normal_deps, vec!["proc_macro_rules"]);
537
538 let proc_macro_deps: Vec<_> = dependencies
539 .proc_macro_deps
540 .items()
541 .into_iter()
542 .map(|(_, dep)| dep.target_name)
543 .collect();
544 assert_eq!(proc_macro_deps, Vec::<&str>::new());
545 }
546
547 #[test]
bench_name_alias_dep()548 fn bench_name_alias_dep() {
549 let metadata = metadata::alias();
550
551 let node = find_metadata_node("surrealdb-core", &metadata);
552 let dependencies =
553 DependencySet::new_for_node(node, &metadata, &TreeResolverMetadata::default());
554
555 let bindings = dependencies.normal_deps.items();
556
557 // It's critical that the dep be found with the correct name and not the
558 // alias that the `aliases` package is using that coincidentally matches the
559 // `bench` target `executor` in the `async-executor` package.
560 let async_executor = bindings
561 .iter()
562 .find(|(_, dep)| dep.target_name == "async_executor")
563 .map(|(_, dep)| dep)
564 .unwrap();
565
566 // Ensure alias data is still tracked.
567 assert_eq!(async_executor.alias, Some("executor".to_owned()));
568 }
569
570 #[test]
sys_dependencies()571 fn sys_dependencies() {
572 let metadata = metadata::build_scripts();
573
574 let openssl_node = find_metadata_node("openssl", &metadata);
575
576 let dependencies =
577 DependencySet::new_for_node(openssl_node, &metadata, &TreeResolverMetadata::default());
578
579 let normal_sys_crate =
580 dependencies
581 .normal_deps
582 .items()
583 .into_iter()
584 .find(|(configuration, dep)| {
585 let pkg = &metadata[&dep.package_id];
586 configuration.is_none() && pkg.name == "openssl-sys"
587 });
588
589 let link_dep_sys_crate =
590 dependencies
591 .build_link_deps
592 .items()
593 .into_iter()
594 .find(|(configuration, dep)| {
595 let pkg = &metadata[&dep.package_id];
596 configuration.is_none() && pkg.name == "openssl-sys"
597 });
598
599 // sys crates like `openssl-sys` should always be dependencies of any
600 // crate which matches it's name minus the `-sys` suffix
601 assert!(normal_sys_crate.is_some());
602 assert!(link_dep_sys_crate.is_some());
603 }
604
605 #[test]
sys_crate_with_build_script()606 fn sys_crate_with_build_script() {
607 let metadata = metadata::build_scripts();
608
609 let libssh2 = find_metadata_node("libssh2-sys", &metadata);
610 let libssh2_depset =
611 DependencySet::new_for_node(libssh2, &metadata, &TreeResolverMetadata::default());
612
613 // Collect build dependencies into a set
614 let build_deps: BTreeSet<String> = libssh2_depset
615 .build_deps
616 .values()
617 .into_iter()
618 .map(|dep| dep.package_id.repr)
619 .collect();
620
621 assert_eq!(
622 BTreeSet::from([
623 "registry+https://github.com/rust-lang/crates.io-index#[email protected]".to_owned(),
624 "registry+https://github.com/rust-lang/crates.io-index#[email protected]"
625 .to_owned(),
626 "registry+https://github.com/rust-lang/crates.io-index#[email protected]".to_owned()
627 ]),
628 build_deps,
629 );
630
631 // Collect normal dependencies into a set
632 let normal_deps: BTreeSet<String> = libssh2_depset
633 .normal_deps
634 .values()
635 .into_iter()
636 .map(|dep| dep.package_id.to_string())
637 .collect();
638
639 assert_eq!(
640 BTreeSet::from([
641 "registry+https://github.com/rust-lang/crates.io-index#[email protected]".to_owned(),
642 "registry+https://github.com/rust-lang/crates.io-index#[email protected]".to_owned(),
643 "registry+https://github.com/rust-lang/crates.io-index#[email protected]"
644 .to_owned(),
645 ]),
646 normal_deps,
647 );
648
649 assert!(libssh2_depset.proc_macro_deps.is_empty());
650 assert!(libssh2_depset.normal_dev_deps.is_empty());
651 assert!(libssh2_depset.proc_macro_dev_deps.is_empty());
652 assert!(libssh2_depset.build_proc_macro_deps.is_empty());
653 }
654
655 #[test]
tracked_aliases()656 fn tracked_aliases() {
657 let metadata = metadata::alias();
658
659 let aliases_node = find_metadata_node("aliases", &metadata);
660 let dependencies =
661 DependencySet::new_for_node(aliases_node, &metadata, &TreeResolverMetadata::default());
662
663 let aliases: Vec<Dependency> = dependencies
664 .normal_deps
665 .items()
666 .into_iter()
667 .filter(|(configuration, dep)| configuration.is_none() && dep.alias.is_some())
668 .map(|(_, dep)| dep)
669 .collect();
670
671 assert_eq!(aliases.len(), 2);
672
673 let expected: BTreeSet<String> =
674 aliases.into_iter().map(|dep| dep.alias.unwrap()).collect();
675
676 assert_eq!(
677 expected,
678 BTreeSet::from(["pinned_log".to_owned(), "pinned_names".to_owned()])
679 );
680 }
681
682 #[test]
matched_rlib()683 fn matched_rlib() {
684 let metadata = metadata::crate_types();
685
686 let node = find_metadata_node("crate-types", &metadata);
687 let dependencies =
688 DependencySet::new_for_node(node, &metadata, &TreeResolverMetadata::default());
689
690 let rlib_deps: Vec<Dependency> = dependencies
691 .normal_deps
692 .items()
693 .into_iter()
694 .filter(|(configuration, dep)| {
695 let pkg = &metadata[&dep.package_id];
696 configuration.is_none()
697 && pkg
698 .targets
699 .iter()
700 .any(|t| t.crate_types.contains(&"rlib".to_owned()))
701 })
702 .map(|(_, dep)| dep)
703 .collect();
704
705 // Currently the only expected __explicitly__ "rlib" target in this metadata is `sysinfo`.
706 assert_eq!(rlib_deps.len(), 1);
707
708 let sysinfo_dep = rlib_deps.iter().last().unwrap();
709 assert_eq!(sysinfo_dep.target_name, "sysinfo");
710 }
711
712 #[test]
multiple_dep_kinds()713 fn multiple_dep_kinds() {
714 let metadata = metadata::multi_cfg_dep();
715
716 let node = find_metadata_node("cpufeatures", &metadata);
717 let dependencies =
718 DependencySet::new_for_node(node, &metadata, &TreeResolverMetadata::default());
719
720 let libc_cfgs: BTreeSet<Option<String>> = dependencies
721 .normal_deps
722 .items()
723 .into_iter()
724 .filter(|(_, dep)| dep.target_name == "libc")
725 .map(|(configuration, _)| configuration)
726 .collect();
727
728 assert_eq!(
729 BTreeSet::from([
730 Some("aarch64-linux-android".to_owned()),
731 Some("cfg(all(target_arch = \"aarch64\", target_os = \"linux\"))".to_owned()),
732 Some("cfg(all(target_arch = \"aarch64\", target_vendor = \"apple\"))".to_owned()),
733 ]),
734 libc_cfgs,
735 );
736 }
737
738 #[test]
multi_kind_proc_macro_dep()739 fn multi_kind_proc_macro_dep() {
740 let metadata = metadata::multi_kind_proc_macro_dep();
741
742 let node = find_metadata_node("multi-kind-proc-macro-dep", &metadata);
743 let dependencies =
744 DependencySet::new_for_node(node, &metadata, &TreeResolverMetadata::default());
745
746 let lib_deps: Vec<_> = dependencies
747 .proc_macro_deps
748 .items()
749 .into_iter()
750 .map(|(_, dep)| dep.target_name)
751 .collect();
752 assert_eq!(lib_deps, vec!["paste"]);
753
754 let build_deps: Vec<_> = dependencies
755 .build_proc_macro_deps
756 .items()
757 .into_iter()
758 .map(|(_, dep)| dep.target_name)
759 .collect();
760 assert_eq!(build_deps, vec!["paste"]);
761 }
762
763 #[test]
optional_deps_disabled()764 fn optional_deps_disabled() {
765 let metadata = metadata::optional_deps_disabled();
766
767 let node = find_metadata_node("clap", &metadata);
768 let dependencies =
769 DependencySet::new_for_node(node, &metadata, &TreeResolverMetadata::default());
770
771 assert!(!dependencies
772 .normal_deps
773 .items()
774 .iter()
775 .any(|(configuration, dep)| configuration.is_none()
776 && (dep.target_name == "is-terminal" || dep.target_name == "termcolor")));
777 }
778
779 #[test]
renamed_optional_deps_disabled()780 fn renamed_optional_deps_disabled() {
781 let metadata = metadata::renamed_optional_deps_disabled();
782
783 let serde_with = find_metadata_node("serde_with", &metadata);
784 let serde_with_depset =
785 DependencySet::new_for_node(serde_with, &metadata, &TreeResolverMetadata::new());
786 assert!(!serde_with_depset
787 .normal_deps
788 .items()
789 .iter()
790 .any(|(configuration, dep)| configuration.is_none() && dep.target_name == "indexmap"));
791 }
792
793 #[test]
optional_deps_enabled()794 fn optional_deps_enabled() {
795 let metadata = metadata::optional_deps_enabled();
796 let mut select = Select::new();
797 select.insert(
798 CargoTreeEntry {
799 features: BTreeSet::new(),
800 deps: BTreeSet::from([
801 CrateId::new("is-terminal".to_owned(), Version::new(0, 4, 5)),
802 CrateId::new("termcolor".to_owned(), Version::new(1, 2, 0)),
803 ]),
804 },
805 None,
806 );
807 let resolver_data = TreeResolverMetadata::from([(
808 CrateId::new("clap".to_owned(), Version::new(4, 1, 1)),
809 select,
810 )]);
811
812 let clap = find_metadata_node("clap", &metadata);
813 let clap_depset = DependencySet::new_for_node(clap, &metadata, &resolver_data);
814 assert_eq!(
815 clap_depset
816 .normal_deps
817 .items()
818 .iter()
819 .filter(|(configuration, dep)| configuration.is_none()
820 && (dep.target_name == "is_terminal" || dep.target_name == "termcolor"))
821 .count(),
822 2
823 );
824
825 let notify = find_metadata_node("notify", &metadata);
826 let notify_depset =
827 DependencySet::new_for_node(notify, &metadata, &TreeResolverMetadata::default());
828
829 // mio is not present in the common list of dependencies
830 assert!(!notify_depset
831 .normal_deps
832 .items()
833 .iter()
834 .any(|(configuration, dep)| configuration.is_none() && dep.target_name == "mio"));
835
836 // mio is a dependency on linux
837 assert!(notify_depset
838 .normal_deps
839 .items()
840 .iter()
841 .any(|(configuration, dep)| configuration.as_deref()
842 == Some("cfg(target_os = \"linux\")")
843 && dep.target_name == "mio"));
844
845 // mio is marked optional=true on macos
846 assert!(!notify_depset
847 .normal_deps
848 .items()
849 .iter()
850 .any(|(configuration, dep)| configuration.as_deref()
851 == Some("cfg(target_os = \"macos\")")
852 && dep.target_name == "mio"));
853 }
854
855 #[test]
optional_deps_disabled_build_dep_enabled()856 fn optional_deps_disabled_build_dep_enabled() {
857 let metadata = metadata::optional_deps_disabled_build_dep_enabled();
858
859 let node = find_metadata_node("gherkin", &metadata);
860 let dependencies =
861 DependencySet::new_for_node(node, &metadata, &TreeResolverMetadata::default());
862
863 assert!(!dependencies
864 .normal_deps
865 .items()
866 .iter()
867 .any(|(configuration, dep)| configuration.is_none() && dep.target_name == "serde"));
868
869 assert!(dependencies
870 .build_deps
871 .items()
872 .iter()
873 .any(|(configuration, dep)| configuration.is_none() && dep.target_name == "serde"));
874 }
875
876 #[test]
renamed_optional_deps_enabled()877 fn renamed_optional_deps_enabled() {
878 let metadata = metadata::renamed_optional_deps_enabled();
879
880 let mut select = Select::new();
881 select.insert(
882 CargoTreeEntry {
883 features: BTreeSet::new(),
884 deps: BTreeSet::from([CrateId::new("ecdsa".to_owned(), Version::new(0, 16, 8))]),
885 },
886 None,
887 );
888 let resolver_data = TreeResolverMetadata::from([(
889 CrateId::new("p256".to_owned(), Version::new(0, 13, 2)),
890 select,
891 )]);
892
893 let p256 = find_metadata_node("p256", &metadata);
894 let p256_depset = DependencySet::new_for_node(p256, &metadata, &resolver_data);
895 assert_eq!(
896 p256_depset
897 .normal_deps
898 .items()
899 .iter()
900 .filter(|(configuration, dep)| configuration.is_none() && dep.target_name == "ecdsa")
901 .count(),
902 1
903 );
904 }
905
906 #[test]
tree_resolver_deps()907 fn tree_resolver_deps() {
908 let metadata = metadata::resolver_2_deps_metadata();
909
910 let mut select = Select::new();
911 select.insert(
912 CargoTreeEntry {
913 deps: BTreeSet::from([
914 CrateId::new("libc".to_owned(), Version::new(0, 2, 153)),
915 CrateId::new("mio".to_owned(), Version::new(0, 8, 11)),
916 CrateId::new("socket2".to_owned(), Version::new(0, 5, 6)),
917 ]),
918 features: BTreeSet::from([
919 "io-std".to_owned(),
920 "libc".to_owned(),
921 "mio".to_owned(),
922 "net".to_owned(),
923 "rt".to_owned(),
924 "socket2".to_owned(),
925 "sync".to_owned(),
926 "time".to_owned(),
927 ]),
928 },
929 Some("x86_64-unknown-linux-gnu".to_owned()),
930 );
931 select.insert(
932 CargoTreeEntry {
933 deps: BTreeSet::from([
934 CrateId::new("bytes".to_owned(), Version::new(1, 6, 0)),
935 CrateId::new("pin-project-lite".to_owned(), Version::new(0, 2, 14)),
936 ]),
937 features: BTreeSet::from([
938 "bytes".to_owned(),
939 "default".to_owned(),
940 "io-util".to_owned(),
941 ]),
942 },
943 None,
944 );
945
946 let tree_metadata = TreeResolverMetadata::from([(
947 CrateId::new("tokio".to_owned(), Version::new(1, 37, 0)),
948 select,
949 )]);
950
951 let tokio_node = find_metadata_node("tokio", &metadata);
952 let tokio_depset = DependencySet::new_for_node(tokio_node, &metadata, &tree_metadata);
953 assert_eq!(
954 tokio_depset
955 .normal_deps
956 .items()
957 .iter()
958 .filter(|(configuration, dep)| {
959 let is_common = configuration.is_none();
960 let is_mio =
961 dep.target_name == "mio" || dep.package_id.to_string().contains("mio");
962
963 is_common && is_mio
964 })
965 .count(),
966 0,
967 "`mio` is a platform specific dependency and therefore should not be identified under the common configuration."
968 );
969 }
970 }
971