Skip to main content

evoke/
cargo_metadata.rs

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}