1 //! Collect and store information from Cargo metadata specific to Bazel's needs
2
3 use std::collections::{BTreeMap, BTreeSet};
4 use std::path::PathBuf;
5
6 use anyhow::{bail, Result};
7 use cargo_metadata::{Node, Package, PackageId};
8 use hex::ToHex;
9 use serde::{Deserialize, Serialize};
10
11 use crate::config::{Commitish, Config, CrateAnnotations, CrateId};
12 use crate::metadata::dependency::DependencySet;
13 use crate::metadata::TreeResolverMetadata;
14 use crate::splicing::{SourceInfo, WorkspaceMetadata};
15
16 pub(crate) type CargoMetadata = cargo_metadata::Metadata;
17 pub(crate) type CargoLockfile = cargo_lock::Lockfile;
18
19 /// Additional information about a crate relative to other crates in a dependency graph.
20 #[derive(Debug, Serialize, Deserialize)]
21 pub(crate) struct CrateAnnotation {
22 /// The crate's node in the Cargo "resolve" graph.
23 pub(crate) node: Node,
24
25 /// The crate's sorted dependencies.
26 pub(crate) deps: DependencySet,
27 }
28
29 /// Additional information about a Cargo workspace's metadata.
30 #[derive(Debug, Default, Serialize, Deserialize)]
31 pub(crate) struct MetadataAnnotation {
32 /// All packages found within the Cargo metadata
33 pub(crate) packages: BTreeMap<PackageId, Package>,
34
35 /// All [CrateAnnotation]s for all packages
36 pub(crate) crates: BTreeMap<PackageId, CrateAnnotation>,
37
38 /// All packages that are workspace members
39 pub(crate) workspace_members: BTreeSet<PackageId>,
40
41 /// The path to the directory containing the Cargo workspace that produced the metadata.
42 pub(crate) workspace_root: PathBuf,
43
44 /// Information on the Cargo workspace.
45 pub(crate) workspace_metadata: WorkspaceMetadata,
46 }
47
48 impl MetadataAnnotation {
new(metadata: CargoMetadata) -> MetadataAnnotation49 pub(crate) fn new(metadata: CargoMetadata) -> MetadataAnnotation {
50 // UNWRAP: The workspace metadata should be written by a controlled process. This should not return a result
51 let workspace_metadata = find_workspace_metadata(&metadata).unwrap_or_default();
52
53 let resolve = metadata
54 .resolve
55 .as_ref()
56 .expect("The metadata provided requires a resolve graph")
57 .clone();
58
59 let is_node_workspace_member = |node: &Node, metadata: &CargoMetadata| -> bool {
60 metadata.workspace_members.iter().any(|pkg| pkg == &node.id)
61 };
62
63 let workspace_members: BTreeSet<PackageId> = resolve
64 .nodes
65 .iter()
66 .filter(|node| is_node_workspace_member(node, &metadata))
67 .map(|node| node.id.clone())
68 .collect();
69
70 let crates = resolve
71 .nodes
72 .iter()
73 .map(|node| {
74 (
75 node.id.clone(),
76 Self::annotate_crate(
77 node.clone(),
78 &metadata,
79 &workspace_metadata.tree_metadata,
80 ),
81 )
82 })
83 .collect();
84
85 let packages = metadata
86 .packages
87 .into_iter()
88 .map(|pkg| (pkg.id.clone(), pkg))
89 .collect();
90
91 MetadataAnnotation {
92 packages,
93 crates,
94 workspace_members,
95 workspace_root: PathBuf::from(metadata.workspace_root.as_std_path()),
96 workspace_metadata,
97 }
98 }
99
annotate_crate( node: Node, metadata: &CargoMetadata, resolver_data: &TreeResolverMetadata, ) -> CrateAnnotation100 fn annotate_crate(
101 node: Node,
102 metadata: &CargoMetadata,
103 resolver_data: &TreeResolverMetadata,
104 ) -> CrateAnnotation {
105 // Gather all dependencies
106 let deps = DependencySet::new_for_node(&node, metadata, resolver_data);
107
108 CrateAnnotation { node, deps }
109 }
110 }
111
112 /// Additional information about how and where to acquire a crate's source code from.
113 #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
114 pub(crate) enum SourceAnnotation {
115 Git {
116 /// The Git url where to clone the source from.
117 remote: String,
118
119 /// The revision information for the git repository. This is used for
120 /// [git_repository::commit](https://docs.bazel.build/versions/main/repo/git.html#git_repository-commit),
121 /// [git_repository::tag](https://docs.bazel.build/versions/main/repo/git.html#git_repository-tag), or
122 /// [git_repository::branch](https://docs.bazel.build/versions/main/repo/git.html#git_repository-branch).
123 commitish: Commitish,
124
125 /// See [git_repository::shallow_since](https://docs.bazel.build/versions/main/repo/git.html#git_repository-shallow_since)
126 #[serde(default, skip_serializing_if = "Option::is_none")]
127 shallow_since: Option<String>,
128
129 /// See [git_repository::strip_prefix](https://docs.bazel.build/versions/main/repo/git.html#git_repository-strip_prefix)
130 #[serde(default, skip_serializing_if = "Option::is_none")]
131 strip_prefix: Option<String>,
132
133 /// See [git_repository::patch_args](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patch_args)
134 #[serde(default, skip_serializing_if = "Option::is_none")]
135 patch_args: Option<Vec<String>>,
136
137 /// See [git_repository::patch_tool](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patch_tool)
138 #[serde(default, skip_serializing_if = "Option::is_none")]
139 patch_tool: Option<String>,
140
141 /// See [git_repository::patches](https://docs.bazel.build/versions/main/repo/git.html#git_repository-patches)
142 #[serde(default, skip_serializing_if = "Option::is_none")]
143 patches: Option<BTreeSet<String>>,
144 },
145 Http {
146 /// See [http_archive::url](https://docs.bazel.build/versions/main/repo/http.html#http_archive-url)
147 url: String,
148
149 /// See [http_archive::sha256](https://docs.bazel.build/versions/main/repo/http.html#http_archive-sha256)
150 #[serde(default, skip_serializing_if = "Option::is_none")]
151 sha256: Option<String>,
152
153 /// See [http_archive::patch_args](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patch_args)
154 #[serde(default, skip_serializing_if = "Option::is_none")]
155 patch_args: Option<Vec<String>>,
156
157 /// See [http_archive::patch_tool](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patch_tool)
158 #[serde(default, skip_serializing_if = "Option::is_none")]
159 patch_tool: Option<String>,
160
161 /// See [http_archive::patches](https://docs.bazel.build/versions/main/repo/http.html#http_archive-patches)
162 #[serde(default, skip_serializing_if = "Option::is_none")]
163 patches: Option<BTreeSet<String>>,
164 },
165 }
166
167 /// Additional information related to [Cargo.lock](https://doc.rust-lang.org/cargo/guide/cargo-toml-vs-cargo-lock.html)
168 /// data used for improved determinism.
169 #[derive(Debug, Default, PartialEq, Eq, PartialOrd, Ord, Deserialize, Serialize)]
170 pub(crate) struct LockfileAnnotation {
171 /// A mapping of crates/packages to additional source (network location) information.
172 pub(crate) crates: BTreeMap<PackageId, SourceAnnotation>,
173 }
174
175 impl LockfileAnnotation {
new(lockfile: CargoLockfile, metadata: &CargoMetadata) -> Result<Self>176 pub(crate) fn new(lockfile: CargoLockfile, metadata: &CargoMetadata) -> Result<Self> {
177 let workspace_metadata = find_workspace_metadata(metadata).unwrap_or_default();
178
179 let nodes: Vec<&Node> = metadata
180 .resolve
181 .as_ref()
182 .expect("Metadata is expected to have a resolve graph")
183 .nodes
184 .iter()
185 .filter(|node| !is_workspace_member(&node.id, metadata))
186 .collect();
187
188 // Produce source annotations for each crate in the resolve graph
189 let crates = nodes
190 .iter()
191 .map(|node| {
192 Ok((
193 node.id.clone(),
194 Self::collect_source_annotations(
195 node,
196 metadata,
197 &lockfile,
198 &workspace_metadata,
199 )?,
200 ))
201 })
202 .collect::<Result<BTreeMap<PackageId, SourceAnnotation>>>()?;
203
204 Ok(Self { crates })
205 }
206
207 /// Resolve all URLs and checksum-like data for each package
collect_source_annotations( node: &Node, metadata: &CargoMetadata, lockfile: &CargoLockfile, workspace_metadata: &WorkspaceMetadata, ) -> Result<SourceAnnotation>208 fn collect_source_annotations(
209 node: &Node,
210 metadata: &CargoMetadata,
211 lockfile: &CargoLockfile,
212 workspace_metadata: &WorkspaceMetadata,
213 ) -> Result<SourceAnnotation> {
214 let pkg = &metadata[&node.id];
215
216 // Locate the matching lock package for the current crate
217 let lock_pkg = match cargo_meta_pkg_to_locked_pkg(pkg, &lockfile.packages) {
218 Some(lock_pkg) => lock_pkg,
219 None => bail!(
220 "Could not find lockfile entry matching metadata package '{}'",
221 pkg.name
222 ),
223 };
224
225 // Check for spliced information about a crate's network source.
226 let spliced_source_info = Self::find_source_annotation(lock_pkg, workspace_metadata);
227
228 // Parse it's source info. The check above should prevent a panic
229 let source = match lock_pkg.source.as_ref() {
230 Some(source) => source,
231 None => match spliced_source_info {
232 Some(info) => {
233 return Ok(SourceAnnotation::Http {
234 url: info.url,
235 sha256: Some(info.sha256),
236 patch_args: None,
237 patch_tool: None,
238 patches: None,
239 })
240 }
241 None => bail!(
242 "The package '{:?} {:?}' has no source info so no annotation can be made",
243 lock_pkg.name,
244 lock_pkg.version
245 ),
246 },
247 };
248
249 // Handle any git repositories
250 if let Some(git_ref) = source.git_reference() {
251 let strip_prefix = Self::extract_git_strip_prefix(pkg)?;
252
253 return Ok(SourceAnnotation::Git {
254 remote: source.url().to_string(),
255 commitish: source
256 .precise()
257 .map(|rev| Commitish::Rev(rev.to_string()))
258 .unwrap_or(Commitish::from(git_ref.clone())),
259 shallow_since: None,
260 strip_prefix,
261 patch_args: None,
262 patch_tool: None,
263 patches: None,
264 });
265 }
266
267 // One of the last things that should be checked is the spliced source information as
268 // other sources may more accurately represent where a crate should be downloaded.
269 if let Some(info) = spliced_source_info {
270 return Ok(SourceAnnotation::Http {
271 url: info.url,
272 sha256: Some(info.sha256),
273 patch_args: None,
274 patch_tool: None,
275 patches: None,
276 });
277 }
278
279 // Finally, In the event that no spliced source information was included in the
280 // metadata the raw source info is used for registry crates and `crates.io` is
281 // assumed to be the source.
282 if source.is_registry() {
283 // source url
284 return Ok(SourceAnnotation::Http {
285 url: format!(
286 "https://static.crates.io/crates/{}/{}/download",
287 lock_pkg.name, lock_pkg.version
288 ),
289 sha256: lock_pkg
290 .checksum
291 .as_ref()
292 .and_then(|sum| {
293 if sum.is_sha256() {
294 sum.as_sha256()
295 } else {
296 None
297 }
298 })
299 .map(|sum| sum.encode_hex::<String>()),
300 patch_args: None,
301 patch_tool: None,
302 patches: None,
303 });
304 }
305
306 bail!(
307 "Unable to determine source annotation for '{:?} {:?}",
308 lock_pkg.name,
309 lock_pkg.version
310 )
311 }
312
find_source_annotation( package: &cargo_lock::Package, metadata: &WorkspaceMetadata, ) -> Option<SourceInfo>313 fn find_source_annotation(
314 package: &cargo_lock::Package,
315 metadata: &WorkspaceMetadata,
316 ) -> Option<SourceInfo> {
317 let crate_id = CrateId::new(package.name.to_string(), package.version.clone());
318 metadata.sources.get(&crate_id).cloned()
319 }
320
extract_git_strip_prefix(pkg: &Package) -> Result<Option<String>>321 fn extract_git_strip_prefix(pkg: &Package) -> Result<Option<String>> {
322 // {CARGO_HOME}/git/checkouts/name-hash/short-sha/[strip_prefix...]/Cargo.toml
323 let components = pkg
324 .manifest_path
325 .components()
326 .map(|v| v.to_string())
327 .collect::<Vec<_>>();
328 for (i, _) in components.iter().enumerate() {
329 let possible_components = &components[i..];
330 if possible_components.len() < 5 {
331 continue;
332 }
333 if possible_components[0] != "git"
334 || possible_components[1] != "checkouts"
335 || possible_components[possible_components.len() - 1] != "Cargo.toml"
336 {
337 continue;
338 }
339 if possible_components.len() == 5 {
340 return Ok(None);
341 }
342 return Ok(Some(
343 possible_components[4..(possible_components.len() - 1)].join("/"),
344 ));
345 }
346 bail!("Expected git package to have a manifest path of pattern {{CARGO_HOME}}/git/checkouts/[name]-[hash]/[short-sha]/.../Cargo.toml but {:?} had manifest path {}", pkg.id, pkg.manifest_path);
347 }
348 }
349
350 /// A pairing of a crate's package identifier to its annotations.
351 #[derive(Debug)]
352 pub(crate) struct PairedExtras {
353 /// The crate's package identifier
354 pub(crate) package_id: cargo_metadata::PackageId,
355
356 /// The crate's annotations
357 pub(crate) crate_extra: CrateAnnotations,
358 }
359
360 /// A collection of data which has been processed for optimal use in generating Bazel targets.
361 #[derive(Debug, Default)]
362 pub(crate) struct Annotations {
363 /// Annotated Cargo metadata
364 pub(crate) metadata: MetadataAnnotation,
365
366 /// Annotated Cargo lockfile
367 pub(crate) lockfile: LockfileAnnotation,
368
369 /// The current workspace's configuration settings
370 pub(crate) config: Config,
371
372 /// Pairred crate annotations
373 pub(crate) pairred_extras: BTreeMap<CrateId, PairedExtras>,
374 }
375
376 impl Annotations {
new( cargo_metadata: CargoMetadata, cargo_lockfile: CargoLockfile, config: Config, ) -> Result<Self>377 pub(crate) fn new(
378 cargo_metadata: CargoMetadata,
379 cargo_lockfile: CargoLockfile,
380 config: Config,
381 ) -> Result<Self> {
382 let lockfile_annotation = LockfileAnnotation::new(cargo_lockfile, &cargo_metadata)?;
383
384 // Annotate the cargo metadata
385 let metadata_annotation = MetadataAnnotation::new(cargo_metadata);
386
387 let mut unused_extra_annotations = config.annotations.clone();
388
389 // Ensure each override matches a particular package
390 let pairred_extras = metadata_annotation
391 .packages
392 .iter()
393 .filter_map(|(pkg_id, pkg)| {
394 let mut crate_extra: CrateAnnotations = config
395 .annotations
396 .iter()
397 .filter(|(id, _)| id.matches(pkg))
398 .map(|(id, extra)| {
399 // Mark that an annotation has been consumed
400 unused_extra_annotations.remove(id);
401
402 // Filter out the annotation
403 extra
404 })
405 .cloned()
406 .sum();
407
408 crate_extra.apply_defaults_from_package_metadata(&pkg.metadata);
409
410 if crate_extra == CrateAnnotations::default() {
411 None
412 } else {
413 Some((
414 CrateId::new(pkg.name.clone(), pkg.version.clone()),
415 PairedExtras {
416 package_id: pkg_id.clone(),
417 crate_extra,
418 },
419 ))
420 }
421 })
422 .collect();
423
424 // Alert on any unused annotations
425 if !unused_extra_annotations.is_empty() {
426 bail!(
427 "Unused annotations were provided. Please remove them: {:?}",
428 unused_extra_annotations.keys()
429 );
430 }
431
432 // Annotate metadata
433 Ok(Annotations {
434 metadata: metadata_annotation,
435 lockfile: lockfile_annotation,
436 config,
437 pairred_extras,
438 })
439 }
440 }
441
find_workspace_metadata(cargo_metadata: &CargoMetadata) -> Option<WorkspaceMetadata>442 fn find_workspace_metadata(cargo_metadata: &CargoMetadata) -> Option<WorkspaceMetadata> {
443 WorkspaceMetadata::try_from(cargo_metadata.workspace_metadata.clone()).ok()
444 }
445
446 /// Determines whether or not a package is a workspace member. This follows
447 /// the Cargo definition of a workspace memeber with one exception where
448 /// "extra workspace members" are *not* treated as workspace members
is_workspace_member(id: &PackageId, cargo_metadata: &CargoMetadata) -> bool449 fn is_workspace_member(id: &PackageId, cargo_metadata: &CargoMetadata) -> bool {
450 if cargo_metadata.workspace_members.contains(id) {
451 if let Some(data) = find_workspace_metadata(cargo_metadata) {
452 let pkg = &cargo_metadata[id];
453 let crate_id = CrateId::new(pkg.name.clone(), pkg.version.clone());
454
455 !data.sources.contains_key(&crate_id)
456 } else {
457 true
458 }
459 } else {
460 false
461 }
462 }
463
464 /// Match a [cargo_metadata::Package] to a [cargo_lock::Package].
cargo_meta_pkg_to_locked_pkg<'a>( pkg: &Package, lock_packages: &'a [cargo_lock::Package], ) -> Option<&'a cargo_lock::Package>465 fn cargo_meta_pkg_to_locked_pkg<'a>(
466 pkg: &Package,
467 lock_packages: &'a [cargo_lock::Package],
468 ) -> Option<&'a cargo_lock::Package> {
469 lock_packages
470 .iter()
471 .find(|lock_pkg| lock_pkg.name.as_str() == pkg.name && lock_pkg.version == pkg.version)
472 }
473
474 #[cfg(test)]
475 mod test {
476 use super::*;
477
478 use semver::Version;
479 use serde_json::json;
480
481 use crate::config::CrateNameAndVersionReq;
482 use crate::metadata::CargoTreeEntry;
483 use crate::select::Select;
484 use crate::test::*;
485
486 #[test]
test_cargo_meta_pkg_to_locked_pkg()487 fn test_cargo_meta_pkg_to_locked_pkg() {
488 let pkg = mock_cargo_metadata_package();
489 let lock_pkg = mock_cargo_lock_package();
490
491 assert!(cargo_meta_pkg_to_locked_pkg(&pkg, &vec![lock_pkg]).is_some())
492 }
493
494 #[test]
annotate_metadata_with_aliases()495 fn annotate_metadata_with_aliases() {
496 let annotations = MetadataAnnotation::new(test::metadata::alias());
497 let log_crates: BTreeMap<&PackageId, &CrateAnnotation> = annotations
498 .crates
499 .iter()
500 .filter(|(id, _)| {
501 let pkg = &annotations.packages[*id];
502 pkg.name == "log"
503 })
504 .collect();
505
506 assert_eq!(log_crates.len(), 2);
507 }
508
509 #[test]
annotate_lockfile_with_aliases()510 fn annotate_lockfile_with_aliases() {
511 LockfileAnnotation::new(test::lockfile::alias(), &test::metadata::alias()).unwrap();
512 }
513
514 #[test]
annotate_metadata_with_build_scripts()515 fn annotate_metadata_with_build_scripts() {
516 MetadataAnnotation::new(test::metadata::build_scripts());
517 }
518
519 #[test]
annotate_lockfile_with_build_scripts()520 fn annotate_lockfile_with_build_scripts() {
521 LockfileAnnotation::new(
522 test::lockfile::build_scripts(),
523 &test::metadata::build_scripts(),
524 )
525 .unwrap();
526 }
527
528 #[test]
annotate_lockfile_with_no_deps()529 fn annotate_lockfile_with_no_deps() {
530 LockfileAnnotation::new(test::lockfile::no_deps(), &test::metadata::no_deps()).unwrap();
531 }
532
533 #[test]
detects_strip_prefix_for_git_repo()534 fn detects_strip_prefix_for_git_repo() {
535 let crates =
536 LockfileAnnotation::new(test::lockfile::git_repos(), &test::metadata::git_repos())
537 .unwrap()
538 .crates;
539 let tracing_core = crates
540 .iter()
541 .find(|(k, _)| k.repr.contains("#tracing-core@"))
542 .map(|(_, v)| v)
543 .unwrap();
544 match tracing_core {
545 SourceAnnotation::Git {
546 strip_prefix: Some(strip_prefix),
547 ..
548 } if strip_prefix == "tracing-core" => {
549 // Matched correctly.
550 }
551 other => {
552 panic!("Wanted SourceAnnotation::Git with strip_prefix == Some(\"tracing-core\"), got: {:?}", other);
553 }
554 }
555 }
556
557 #[test]
resolves_commit_from_branches_and_tags()558 fn resolves_commit_from_branches_and_tags() {
559 let crates =
560 LockfileAnnotation::new(test::lockfile::git_repos(), &test::metadata::git_repos())
561 .unwrap()
562 .crates;
563
564 let package_id = PackageId {
565 repr: "git+https://github.com/tokio-rs/tracing.git?branch=master#[email protected]".into(),
566 };
567 let annotation = crates.get(&package_id).unwrap();
568
569 let commitish = match annotation {
570 SourceAnnotation::Git { commitish, .. } => commitish,
571 _ => panic!("Unexpected annotation type"),
572 };
573
574 assert_eq!(
575 *commitish,
576 Commitish::Rev("1e09e50e8d15580b5929adbade9c782a6833e4a0".into())
577 );
578 }
579
580 #[test]
detect_unused_annotation()581 fn detect_unused_annotation() {
582 // Create a config with some random annotation
583 let mut config = Config::default();
584 config.annotations.insert(
585 CrateNameAndVersionReq::new("mock-crate".to_owned(), "0.1.0".parse().unwrap()),
586 CrateAnnotations::default(),
587 );
588
589 let result = Annotations::new(test::metadata::no_deps(), test::lockfile::no_deps(), config);
590 assert!(result.is_err());
591
592 let result_str = format!("{result:?}");
593 assert!(result_str.contains("Unused annotations were provided. Please remove them"));
594 assert!(result_str.contains("mock-crate"));
595 }
596
597 #[test]
defaults_from_package_metadata()598 fn defaults_from_package_metadata() {
599 let crate_id = CrateId::new(
600 "has_package_metadata".to_owned(),
601 semver::Version::new(0, 0, 0),
602 );
603 let crate_name_and_version_req = CrateNameAndVersionReq::new(
604 "has_package_metadata".to_owned(),
605 "0.0.0".parse().unwrap(),
606 );
607 let annotations = CrateAnnotations {
608 rustc_env: Some(Select::from_value(BTreeMap::from([(
609 "BAR".to_owned(),
610 "bar is set".to_owned(),
611 )]))),
612 ..CrateAnnotations::default()
613 };
614
615 let mut config = Config::default();
616 config
617 .annotations
618 .insert(crate_name_and_version_req, annotations.clone());
619
620 // Combine the above annotations with default values provided by the
621 // crate author in package metadata.
622 let combined_annotations = Annotations::new(
623 test::metadata::has_package_metadata(),
624 test::lockfile::has_package_metadata(),
625 config,
626 )
627 .unwrap();
628
629 let extras = &combined_annotations.pairred_extras[&crate_id].crate_extra;
630 let expected = CrateAnnotations {
631 // This comes from has_package_metadata's [package.metadata.bazel].
632 additive_build_file_content: Some("genrule(**kwargs)\n".to_owned()),
633 // The package metadata defines a default rustc_env containing FOO,
634 // but it is superseded by a rustc_env annotation containing only
635 // BAR. These dictionaries are intentionally not merged together.
636 ..annotations
637 };
638 assert_eq!(*extras, expected);
639 }
640
641 #[test]
test_find_workspace_metadata()642 fn test_find_workspace_metadata() {
643 let mut metadata = metadata::common();
644 metadata.workspace_metadata = json!({
645 "cargo-bazel": {
646 "package_prefixes": {},
647 "sources": {},
648 "tree_metadata": {
649 "bitflags 1.3.2": {
650 "common": {
651 "features": [
652 "default",
653 ],
654 },
655 "selects": {
656 "x86_64-unknown-linux-gnu": {
657 "features": [
658 "std",
659 ],
660 "deps": [
661 "libc 1.2.3",
662 ],
663 },
664 }
665 }
666 },
667 }
668 });
669
670 let mut select = Select::new();
671 select.insert(
672 CargoTreeEntry {
673 features: BTreeSet::from(["default".to_owned()]),
674 deps: BTreeSet::new(),
675 },
676 None,
677 );
678 select.insert(
679 CargoTreeEntry {
680 features: BTreeSet::from(["std".to_owned()]),
681 deps: BTreeSet::from([CrateId::new("libc".to_owned(), Version::new(1, 2, 3))]),
682 },
683 Some("x86_64-unknown-linux-gnu".to_owned()),
684 );
685 let expected = TreeResolverMetadata::from([(
686 CrateId::new("bitflags".to_owned(), Version::new(1, 3, 2)),
687 select,
688 )]);
689
690 let result = find_workspace_metadata(&metadata).unwrap();
691
692 assert_eq!(expected, result.tree_metadata);
693 }
694 }
695