1 // Copyright (C) 2023 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 //! Types for parsing cargo.metadata JSON files.
16
17 use super::{Crate, CrateType, Extern, ExternType};
18 use crate::config::VariantConfig;
19 use anyhow::{bail, Context, Result};
20 use serde::Deserialize;
21 use std::collections::BTreeMap;
22 use std::path::{Path, PathBuf};
23
24 /// `cfg` strings for dependencies which should be considered enabled. It would be better to parse
25 /// them properly, but this is good enough in practice so far.
26 const ENABLED_CFGS: [&str; 6] = [
27 r#"unix"#,
28 r#"not(windows)"#,
29 r#"any(unix, target_os = "wasi")"#,
30 r#"not(all(target_family = "wasm", target_os = "unknown"))"#,
31 r#"not(target_family = "wasm")"#,
32 r#"any(target_os = "linux", target_os = "android")"#,
33 ];
34
35 /// `cargo metadata` output.
36 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
37 pub struct WorkspaceMetadata {
38 pub packages: Vec<PackageMetadata>,
39 pub workspace_members: Vec<String>,
40 }
41
42 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
43 pub struct PackageMetadata {
44 pub name: String,
45 pub version: String,
46 pub edition: String,
47 pub manifest_path: String,
48 pub dependencies: Vec<DependencyMetadata>,
49 pub features: BTreeMap<String, Vec<String>>,
50 pub id: String,
51 pub targets: Vec<TargetMetadata>,
52 pub license: Option<String>,
53 pub license_file: Option<String>,
54 }
55
56 #[derive(Clone, Debug, Deserialize, Eq, PartialEq)]
57 pub struct DependencyMetadata {
58 pub name: String,
59 pub kind: Option<String>,
60 pub optional: bool,
61 pub target: Option<String>,
62 pub rename: Option<String>,
63 }
64
65 impl DependencyMetadata {
66 /// Returns whether the dependency should be included when the given features are enabled.
enabled(&self, features: &[String], cfgs: &[String]) -> bool67 fn enabled(&self, features: &[String], cfgs: &[String]) -> bool {
68 if let Some(target) = &self.target {
69 if target.starts_with("cfg(") && target.ends_with(')') {
70 let target_cfg = &target[4..target.len() - 1];
71 if !ENABLED_CFGS.contains(&target_cfg) && !cfgs.contains(&target_cfg.to_string()) {
72 return false;
73 }
74 }
75 }
76 let name = self.rename.as_ref().unwrap_or(&self.name);
77 !self.optional || features.contains(&format!("dep:{}", name))
78 }
79 }
80
81 #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq)]
82 #[allow(dead_code)]
83 pub struct TargetMetadata {
84 pub crate_types: Vec<CrateType>,
85 pub doc: bool,
86 pub doctest: bool,
87 pub edition: String,
88 pub kind: Vec<TargetKind>,
89 pub name: String,
90 pub src_path: PathBuf,
91 pub test: bool,
92 }
93
94 #[derive(Copy, Clone, Debug, Deserialize, Eq, PartialEq)]
95 #[serde[rename_all = "kebab-case"]]
96 pub enum TargetKind {
97 Bin,
98 CustomBuild,
99 Bench,
100 Example,
101 Lib,
102 Rlib,
103 Staticlib,
104 Cdylib,
105 ProcMacro,
106 Test,
107 }
108
parse_cargo_metadata_str(cargo_metadata: &str, cfg: &VariantConfig) -> Result<Vec<Crate>>109 pub fn parse_cargo_metadata_str(cargo_metadata: &str, cfg: &VariantConfig) -> Result<Vec<Crate>> {
110 let metadata =
111 serde_json::from_str(cargo_metadata).context("failed to parse cargo metadata")?;
112 parse_cargo_metadata(
113 &metadata,
114 &cfg.features,
115 &cfg.extra_cfg,
116 cfg.tests,
117 &cfg.workspace_excludes,
118 )
119 }
120
parse_cargo_metadata( metadata: &WorkspaceMetadata, features: &Option<Vec<String>>, cfgs: &[String], include_tests: bool, workspace_excludes: &[String], ) -> Result<Vec<Crate>>121 fn parse_cargo_metadata(
122 metadata: &WorkspaceMetadata,
123 features: &Option<Vec<String>>,
124 cfgs: &[String],
125 include_tests: bool,
126 workspace_excludes: &[String],
127 ) -> Result<Vec<Crate>> {
128 let mut crates = Vec::new();
129 for package in &metadata.packages {
130 if !metadata.workspace_members.contains(&package.id)
131 || workspace_excludes.contains(&package.name)
132 {
133 continue;
134 }
135
136 let features = resolve_features(features, &package.features, &package.dependencies);
137 let features_without_deps: Vec<String> =
138 features.clone().into_iter().filter(|feature| !feature.starts_with("dep:")).collect();
139 let package_dir = package_dir_from_id(&package.id)?;
140
141 for target in &package.targets {
142 let target_kinds = target
143 .kind
144 .clone()
145 .into_iter()
146 .filter(|kind| {
147 [
148 TargetKind::Bin,
149 TargetKind::Cdylib,
150 TargetKind::Lib,
151 TargetKind::ProcMacro,
152 TargetKind::Rlib,
153 TargetKind::Staticlib,
154 TargetKind::Test,
155 ]
156 .contains(kind)
157 })
158 .collect::<Vec<_>>();
159 if target_kinds.is_empty() {
160 // Only binaries, libraries and integration tests are supported.
161 continue;
162 }
163 let main_src = split_src_path(&target.src_path, &package_dir);
164 // Hypens are not allowed in crate names. See
165 // https://github.com/rust-lang/rfcs/blob/master/text/0940-hyphens-considered-harmful.md
166 // for background.
167 let target_name = target.name.replace('-', "_");
168 let target_triple = if target_kinds == [TargetKind::ProcMacro] {
169 None
170 } else {
171 Some("x86_64-unknown-linux-gnu".to_string())
172 };
173 // Don't generate an entry for integration tests, they will be covered by the test case
174 // below.
175 if target_kinds != [TargetKind::Test] {
176 crates.push(Crate {
177 name: target_name.clone(),
178 package_name: package.name.to_owned(),
179 version: Some(package.version.to_owned()),
180 types: target.crate_types.clone(),
181 features: features_without_deps.clone(),
182 edition: package.edition.to_owned(),
183 license: package.license.clone(),
184 license_file: package.license_file.clone(),
185 package_dir: package_dir.clone(),
186 main_src: main_src.to_owned(),
187 target: target_triple.clone(),
188 externs: get_externs(
189 package,
190 &metadata.packages,
191 &features,
192 cfgs,
193 &target_kinds,
194 false,
195 )?,
196 cfgs: cfgs.to_owned(),
197 ..Default::default()
198 });
199 }
200 // This includes both unit tests and integration tests.
201 if target.test && include_tests {
202 crates.push(Crate {
203 name: target_name,
204 package_name: package.name.to_owned(),
205 version: Some(package.version.to_owned()),
206 types: vec![CrateType::Test],
207 features: features_without_deps.clone(),
208 edition: package.edition.to_owned(),
209 license: package.license.clone(),
210 license_file: package.license_file.clone(),
211 package_dir: package_dir.clone(),
212 main_src: main_src.to_owned(),
213 target: target_triple.clone(),
214 externs: get_externs(
215 package,
216 &metadata.packages,
217 &features,
218 cfgs,
219 &target_kinds,
220 true,
221 )?,
222 cfgs: cfgs.to_owned(),
223 ..Default::default()
224 });
225 }
226 }
227 }
228 Ok(crates)
229 }
230
get_externs( package: &PackageMetadata, packages: &[PackageMetadata], features: &[String], cfgs: &[String], target_kinds: &[TargetKind], test: bool, ) -> Result<Vec<Extern>>231 fn get_externs(
232 package: &PackageMetadata,
233 packages: &[PackageMetadata],
234 features: &[String],
235 cfgs: &[String],
236 target_kinds: &[TargetKind],
237 test: bool,
238 ) -> Result<Vec<Extern>> {
239 let mut externs = package
240 .dependencies
241 .iter()
242 .filter_map(|dependency| {
243 // Kind is None for normal dependencies, as opposed to dev dependencies.
244 if dependency.enabled(features, cfgs)
245 && dependency.kind.as_deref() != Some("build")
246 && (dependency.kind.is_none() || test)
247 {
248 Some(make_extern(packages, dependency))
249 } else {
250 None
251 }
252 })
253 .collect::<Result<Vec<Extern>>>()?;
254
255 // If there is a library target and this is a binary or integration test, add the library as an
256 // extern.
257 if matches!(target_kinds, [TargetKind::Bin] | [TargetKind::Test]) {
258 for target in &package.targets {
259 if target.kind.contains(&TargetKind::Lib) {
260 let lib_name = target.name.replace('-', "_");
261 externs.push(Extern {
262 name: lib_name.clone(),
263 lib_name,
264 raw_name: target.name.clone(),
265 extern_type: ExternType::Rust,
266 });
267 }
268 }
269 }
270
271 externs.sort();
272 externs.dedup();
273 Ok(externs)
274 }
275
make_extern(packages: &[PackageMetadata], dependency: &DependencyMetadata) -> Result<Extern>276 fn make_extern(packages: &[PackageMetadata], dependency: &DependencyMetadata) -> Result<Extern> {
277 let Some(package) = packages.iter().find(|package| package.name == dependency.name) else {
278 bail!("package {} not found in metadata", dependency.name);
279 };
280 let Some(target) = package.targets.iter().find(|target| {
281 target.kind.contains(&TargetKind::Lib) || target.kind.contains(&TargetKind::ProcMacro)
282 }) else {
283 bail!("Package {} didn't have any library or proc-macro targets", dependency.name);
284 };
285 let lib_name = target.name.replace('-', "_");
286 // This is ugly but looking at the source path is the easiest way to tell if the raw
287 // crate name uses a hyphen instead of an underscore. It won't work if it uses both.
288 let raw_name = target.name.replace('_', "-");
289 let src_path = target.src_path.to_str().expect("failed to convert src_path to string");
290 let raw_name = if src_path.contains(&raw_name) { raw_name } else { lib_name.clone() };
291 let name =
292 if let Some(rename) = &dependency.rename { rename.clone() } else { lib_name.clone() };
293
294 // Check whether the package is a proc macro.
295 let extern_type =
296 if package.targets.iter().any(|target| target.kind.contains(&TargetKind::ProcMacro)) {
297 ExternType::ProcMacro
298 } else {
299 ExternType::Rust
300 };
301 Ok(Extern { name, lib_name, raw_name, extern_type })
302 }
303
304 /// Given a Cargo package ID, returns the path.
305 ///
306 /// Extracts `"/path/to/crate"` from
307 /// `"path+file:///path/to/crate#1.2.3"`. See
308 /// https://doc.rust-lang.org/cargo/reference/pkgid-spec.html for
309 /// information on Cargo package ID specifications.
package_dir_from_id(id: &str) -> Result<PathBuf>310 fn package_dir_from_id(id: &str) -> Result<PathBuf> {
311 const PREFIX: &str = "path+file://";
312 const SEPARATOR: char = '#';
313 let Some(stripped) = id.strip_prefix(PREFIX) else {
314 bail!("Invalid package ID {id:?}, expected it to start with {PREFIX:?}");
315 };
316 let Some(idx) = stripped.rfind(SEPARATOR) else {
317 bail!("Invalid package ID {id:?}, expected it to contain {SEPARATOR:?}");
318 };
319 Ok(PathBuf::from(stripped[..idx].to_string()))
320 }
321
split_src_path<'a>(src_path: &'a Path, package_dir: &Path) -> &'a Path322 fn split_src_path<'a>(src_path: &'a Path, package_dir: &Path) -> &'a Path {
323 if let Ok(main_src) = src_path.strip_prefix(package_dir) {
324 main_src
325 } else {
326 src_path
327 }
328 }
329
330 /// Given a set of chosen features, and the feature dependencies from a package's metadata, returns
331 /// the full set of features which should be enabled.
resolve_features( chosen_features: &Option<Vec<String>>, package_features: &BTreeMap<String, Vec<String>>, dependencies: &[DependencyMetadata], ) -> Vec<String>332 fn resolve_features(
333 chosen_features: &Option<Vec<String>>,
334 package_features: &BTreeMap<String, Vec<String>>,
335 dependencies: &[DependencyMetadata],
336 ) -> Vec<String> {
337 let mut package_features = package_features.to_owned();
338 // Add implicit features for optional dependencies.
339 for dependency in dependencies {
340 if dependency.optional && !package_features.contains_key(&dependency.name) {
341 package_features
342 .insert(dependency.name.to_owned(), vec![format!("dep:{}", dependency.name)]);
343 }
344 }
345
346 let mut features = Vec::new();
347 if let Some(chosen_features) = chosen_features {
348 for feature in chosen_features {
349 add_feature_and_dependencies(&mut features, feature, &package_features);
350 }
351 } else {
352 // If there are no chosen features, then enable the default feature.
353 add_feature_and_dependencies(&mut features, "default", &package_features);
354 }
355 features.sort();
356 features.dedup();
357 features
358 }
359
360 /// Adds the given feature and all features it depends on to the given list of features.
361 ///
362 /// Ignores features of other packages, and features which don't exist.
add_feature_and_dependencies( features: &mut Vec<String>, feature: &str, package_features: &BTreeMap<String, Vec<String>>, )363 fn add_feature_and_dependencies(
364 features: &mut Vec<String>,
365 feature: &str,
366 package_features: &BTreeMap<String, Vec<String>>,
367 ) {
368 if package_features.contains_key(feature) || feature.starts_with("dep:") {
369 features.push(feature.to_owned());
370 }
371
372 if let Some(dependencies) = package_features.get(feature) {
373 for dependency in dependencies {
374 if let Some((dependency_package, _)) = dependency.split_once('/') {
375 add_feature_and_dependencies(features, dependency_package, package_features);
376 } else {
377 add_feature_and_dependencies(features, dependency, package_features);
378 }
379 }
380 }
381 }
382
383 #[cfg(test)]
384 mod tests {
385 use super::*;
386 use crate::config::Config;
387 use crate::tests::testdata_directories;
388 use googletest::matchers::eq;
389 use googletest::prelude::assert_that;
390 use std::fs::{read_to_string, File};
391
392 #[test]
extract_package_dir_from_id() -> Result<()>393 fn extract_package_dir_from_id() -> Result<()> {
394 assert_eq!(
395 package_dir_from_id("path+file:///path/to/crate#1.2.3")?,
396 PathBuf::from("/path/to/crate")
397 );
398 Ok(())
399 }
400
401 #[test]
resolve_multi_level_feature_dependencies()402 fn resolve_multi_level_feature_dependencies() {
403 let chosen = vec!["default".to_string(), "extra".to_string(), "on_by_default".to_string()];
404 let package_features = [
405 (
406 "default".to_string(),
407 vec!["std".to_string(), "other".to_string(), "on_by_default".to_string()],
408 ),
409 ("std".to_string(), vec!["alloc".to_string()]),
410 ("not_enabled".to_string(), vec![]),
411 ("on_by_default".to_string(), vec![]),
412 ("other".to_string(), vec![]),
413 ("extra".to_string(), vec![]),
414 ("alloc".to_string(), vec![]),
415 ]
416 .into_iter()
417 .collect();
418 assert_eq!(
419 resolve_features(&Some(chosen), &package_features, &[]),
420 vec![
421 "alloc".to_string(),
422 "default".to_string(),
423 "extra".to_string(),
424 "on_by_default".to_string(),
425 "other".to_string(),
426 "std".to_string(),
427 ]
428 );
429 }
430
431 #[test]
resolve_dep_features()432 fn resolve_dep_features() {
433 let package_features = [(
434 "default".to_string(),
435 vec![
436 "optionaldep/feature".to_string(),
437 "requireddep/feature".to_string(),
438 "optionaldep2?/feature".to_string(),
439 ],
440 )]
441 .into_iter()
442 .collect();
443 let dependencies = vec![
444 DependencyMetadata {
445 name: "optionaldep".to_string(),
446 kind: None,
447 optional: true,
448 target: None,
449 rename: None,
450 },
451 DependencyMetadata {
452 name: "optionaldep2".to_string(),
453 kind: None,
454 optional: true,
455 target: None,
456 rename: None,
457 },
458 DependencyMetadata {
459 name: "requireddep".to_string(),
460 kind: None,
461 optional: false,
462 target: None,
463 rename: None,
464 },
465 ];
466 assert_eq!(
467 resolve_features(&None, &package_features, &dependencies),
468 vec!["default".to_string(), "dep:optionaldep".to_string(), "optionaldep".to_string()]
469 );
470 }
471
472 #[test]
get_externs_cfg()473 fn get_externs_cfg() {
474 let package = PackageMetadata {
475 name: "test_package".to_string(),
476 dependencies: vec![
477 DependencyMetadata {
478 name: "alwayslib".to_string(),
479 kind: None,
480 optional: false,
481 target: None,
482 rename: None,
483 },
484 DependencyMetadata {
485 name: "unixlib".to_string(),
486 kind: None,
487 optional: false,
488 target: Some("cfg(unix)".to_string()),
489 rename: None,
490 },
491 DependencyMetadata {
492 name: "windowslib".to_string(),
493 kind: None,
494 optional: false,
495 target: Some("cfg(windows)".to_string()),
496 rename: None,
497 },
498 ],
499 features: [].into_iter().collect(),
500 targets: vec![],
501 ..Default::default()
502 };
503 let packages = vec![
504 package.clone(),
505 PackageMetadata {
506 name: "alwayslib".to_string(),
507 targets: vec![TargetMetadata {
508 name: "alwayslib".to_string(),
509 kind: vec![TargetKind::Lib],
510 ..Default::default()
511 }],
512 ..Default::default()
513 },
514 PackageMetadata {
515 name: "unixlib".to_string(),
516 targets: vec![TargetMetadata {
517 name: "unixlib".to_string(),
518 kind: vec![TargetKind::Lib],
519 ..Default::default()
520 }],
521 ..Default::default()
522 },
523 PackageMetadata {
524 name: "windowslib".to_string(),
525 targets: vec![TargetMetadata {
526 name: "windowslib".to_string(),
527 kind: vec![TargetKind::Lib],
528 ..Default::default()
529 }],
530 ..Default::default()
531 },
532 ];
533 assert_eq!(
534 get_externs(&package, &packages, &[], &[], &[], false).unwrap(),
535 vec![
536 Extern {
537 name: "alwayslib".to_string(),
538 lib_name: "alwayslib".to_string(),
539 raw_name: "alwayslib".to_string(),
540 extern_type: ExternType::Rust
541 },
542 Extern {
543 name: "unixlib".to_string(),
544 lib_name: "unixlib".to_string(),
545 raw_name: "unixlib".to_string(),
546 extern_type: ExternType::Rust
547 },
548 ]
549 );
550 }
551
552 #[test]
get_externs_extra_cfg()553 fn get_externs_extra_cfg() {
554 let package = PackageMetadata {
555 name: "test_package".to_string(),
556 dependencies: vec![
557 DependencyMetadata {
558 name: "foolib".to_string(),
559 kind: None,
560 optional: false,
561 target: Some("cfg(foo)".to_string()),
562 rename: None,
563 },
564 DependencyMetadata {
565 name: "barlib".to_string(),
566 kind: None,
567 optional: false,
568 target: Some("cfg(bar)".to_string()),
569 rename: None,
570 },
571 ],
572 features: [].into_iter().collect(),
573 targets: vec![],
574 ..Default::default()
575 };
576 let packages = vec![
577 package.clone(),
578 PackageMetadata {
579 name: "foolib".to_string(),
580 targets: vec![TargetMetadata {
581 name: "foolib".to_string(),
582 kind: vec![TargetKind::Lib],
583 ..Default::default()
584 }],
585 ..Default::default()
586 },
587 PackageMetadata {
588 name: "barlib".to_string(),
589 targets: vec![TargetMetadata {
590 name: "barlib".to_string(),
591 kind: vec![TargetKind::Lib],
592 ..Default::default()
593 }],
594 ..Default::default()
595 },
596 ];
597 assert_eq!(
598 get_externs(&package, &packages, &[], &["foo".to_string()], &[], false).unwrap(),
599 vec![Extern {
600 name: "foolib".to_string(),
601 lib_name: "foolib".to_string(),
602 raw_name: "foolib".to_string(),
603 extern_type: ExternType::Rust
604 },]
605 );
606 }
607
608 #[test]
get_externs_rename()609 fn get_externs_rename() {
610 let package = PackageMetadata {
611 name: "test_package".to_string(),
612 dependencies: vec![
613 DependencyMetadata {
614 name: "foo".to_string(),
615 kind: None,
616 optional: false,
617 target: None,
618 rename: Some("foo2".to_string()),
619 },
620 DependencyMetadata {
621 name: "bar".to_string(),
622 kind: None,
623 optional: true,
624 target: None,
625 rename: None,
626 },
627 DependencyMetadata {
628 name: "bar".to_string(),
629 kind: None,
630 optional: true,
631 target: None,
632 rename: Some("baz".to_string()),
633 },
634 ],
635 ..Default::default()
636 };
637 let packages = vec![
638 package.clone(),
639 PackageMetadata {
640 name: "foo".to_string(),
641 targets: vec![TargetMetadata {
642 name: "foo".to_string(),
643 kind: vec![TargetKind::Lib],
644 ..Default::default()
645 }],
646 ..Default::default()
647 },
648 PackageMetadata {
649 name: "bar".to_string(),
650 targets: vec![TargetMetadata {
651 name: "bar".to_string(),
652 kind: vec![TargetKind::Lib],
653 ..Default::default()
654 }],
655 ..Default::default()
656 },
657 ];
658 assert_eq!(
659 get_externs(&package, &packages, &["dep:bar".to_string()], &[], &[], false).unwrap(),
660 vec![
661 Extern {
662 name: "bar".to_string(),
663 lib_name: "bar".to_string(),
664 raw_name: "bar".to_string(),
665 extern_type: ExternType::Rust
666 },
667 Extern {
668 name: "foo2".to_string(),
669 lib_name: "foo".to_string(),
670 raw_name: "foo".to_string(),
671 extern_type: ExternType::Rust
672 },
673 ]
674 );
675 assert_eq!(
676 get_externs(&package, &packages, &["dep:baz".to_string()], &[], &[], false).unwrap(),
677 vec![
678 Extern {
679 name: "baz".to_string(),
680 lib_name: "bar".to_string(),
681 raw_name: "bar".to_string(),
682 extern_type: ExternType::Rust
683 },
684 Extern {
685 name: "foo2".to_string(),
686 lib_name: "foo".to_string(),
687 raw_name: "foo".to_string(),
688 extern_type: ExternType::Rust
689 },
690 ]
691 );
692 }
693
694 #[test]
parse_metadata()695 fn parse_metadata() {
696 /// Remove anything before "external/rust/crates/" from the
697 /// `package_dir` field. This makes the test robust since you
698 /// can use `cargo metadata` to regenerate the test files and
699 /// you don't have to care about where your AOSP checkout
700 /// lives.
701 fn normalize_package_dir(mut c: Crate) -> Crate {
702 const EXTERNAL_RUST_CRATES: &str = "external/rust/crates/";
703 let package_dir = c.package_dir.to_str().unwrap();
704 if let Some(idx) = package_dir.find(EXTERNAL_RUST_CRATES) {
705 c.package_dir = PathBuf::from(format!(".../{}", &package_dir[idx..]));
706 }
707 c
708 }
709
710 for testdata_directory_path in testdata_directories() {
711 let cfg = Config::from_json_str(
712 &read_to_string(testdata_directory_path.join("cargo_embargo.json"))
713 .with_context(|| {
714 format!(
715 "Failed to open {:?}",
716 testdata_directory_path.join("cargo_embargo.json")
717 )
718 })
719 .unwrap(),
720 )
721 .unwrap();
722 let cargo_metadata_path = testdata_directory_path.join("cargo.metadata");
723 let expected_crates: Vec<Vec<Crate>> = serde_json::from_reader::<_, Vec<Vec<Crate>>>(
724 File::open(testdata_directory_path.join("crates.json")).unwrap(),
725 )
726 .unwrap()
727 .into_iter()
728 .map(|crates: Vec<Crate>| crates.into_iter().map(normalize_package_dir).collect())
729 .collect();
730
731 let crates = cfg
732 .variants
733 .iter()
734 .map(|variant_cfg| {
735 parse_cargo_metadata_str(
736 &read_to_string(&cargo_metadata_path)
737 .with_context(|| format!("Failed to open {:?}", cargo_metadata_path))
738 .unwrap(),
739 variant_cfg,
740 )
741 .unwrap()
742 .into_iter()
743 .map(normalize_package_dir)
744 .collect::<Vec<Crate>>()
745 })
746 .collect::<Vec<Vec<Crate>>>();
747 assert_that!(format!("{crates:#?}"), eq(format!("{expected_crates:#?}")));
748 }
749 }
750 }
751