1use std::path::Path;
2use std::path::PathBuf;
3use std::process::Command;
4
5use rootcause::prelude::ResultExt;
6use serde::Deserialize;
7
8#[derive(Deserialize)]
9pub struct Metadata {
10 packages: Vec<Package>,
11}
12
13impl Metadata {
14 pub fn read(workspace_root: &Path) -> rootcause::Result<Self> {
15 let mut command = Command::new("cargo");
16 command
17 .args(["metadata", "--format-version=1", "--no-deps"])
18 .current_dir(workspace_root);
19 let output = command.output().context("failed to spawn cargo metadata")?;
20 output.status.exit_ok().context("cargo metadata failed")?;
21 Self::from_slice(&output.stdout)
22 }
23
24 pub fn native_audit_bin_paths(&self) -> Vec<PathBuf> {
25 let mut paths = self
26 .packages
27 .iter()
28 .flat_map(|package| package.targets.iter())
29 .filter(|target| target.is_default_bin())
30 .map(|target| Path::new("./target/release").join(&target.name))
31 .collect::<Vec<_>>();
32 paths.sort();
33 paths
34 }
35
36 pub fn native_bin_package_names(&self) -> Vec<&str> {
37 let mut package_names = self
38 .packages
39 .iter()
40 .filter(|package| package.targets.iter().any(Target::is_default_bin))
41 .map(|package| package.name.as_str())
42 .collect::<Vec<_>>();
43 package_names.sort_unstable();
44 package_names
45 }
46
47 fn from_slice(metadata: &[u8]) -> rootcause::Result<Self> {
48 Ok(serde_json::from_slice::<Self>(metadata).context("failed to parse cargo metadata")?)
49 }
50}
51
52#[derive(Deserialize)]
53struct Package {
54 name: String,
55 targets: Vec<Target>,
56}
57
58#[derive(Deserialize)]
59struct Target {
60 kind: Vec<String>,
61 name: String,
62 #[serde(default, rename = "required-features")]
63 required_features: Vec<String>,
64}
65
66impl Target {
67 fn is_default_bin(&self) -> bool {
68 self.kind.iter().any(|kind| kind == "bin") && self.required_features.is_empty()
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use test_that::prelude::*;
75
76 use super::*;
77
78 #[test]
79 fn test_native_audit_bin_paths_uses_native_bin_targets() {
80 assert_that!(
81 Metadata::from_slice(metadata_fixture()).map(|metadata| metadata.native_audit_bin_paths()),
82 ok(eq(vec![
83 PathBuf::from("./target/release/evoke"),
84 PathBuf::from("./target/release/fixture-tool"),
85 PathBuf::from("./target/release/helper-cli"),
86 ]))
87 );
88 }
89
90 #[test]
91 fn test_native_bin_package_names_uses_native_bin_targets() {
92 assert_that!(
93 Metadata::from_slice(metadata_fixture()).map(|metadata| {
94 metadata
95 .native_bin_package_names()
96 .into_iter()
97 .map(str::to_owned)
98 .collect::<Vec<_>>()
99 }),
100 ok(eq(vec!["evoke", "fixture-tool", "helper-cli"]))
101 );
102 }
103
104 fn metadata_fixture() -> &'static [u8] {
105 br#"
106 {
107 "packages": [
108 {
109 "dependencies": [],
110 "manifest_path": "/repo/yog/evoke/Cargo.toml",
111 "name": "evoke",
112 "targets": [
113 { "kind": ["bin"], "name": "evoke" },
114 { "kind": ["lib"], "name": "evoke" }
115 ]
116 },
117 {
118 "dependencies": [],
119 "manifest_path": "/repo/yog/helper-cli/Cargo.toml",
120 "name": "helper-cli",
121 "targets": [
122 { "kind": ["bin"], "name": "helper-cli" }
123 ]
124 },
125 {
126 "dependencies": [],
127 "manifest_path": "/repo/yog/feature-bin/Cargo.toml",
128 "name": "feature-bin",
129 "targets": [
130 {
131 "kind": ["bin"],
132 "name": "feature-bin",
133 "required-features": ["cli"]
134 }
135 ]
136 },
137 {
138 "dependencies": [
139 { "kind": "dev", "name": "fixture-dev-dep" }
140 ],
141 "manifest_path": "/repo/yog/fixture-tool/Cargo.toml",
142 "name": "fixture-tool",
143 "targets": [
144 { "kind": ["bin"], "name": "fixture-tool" }
145 ]
146 }
147 ]
148 }
149 "#
150 }
151}