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/// Dependency that identifies a workspace package as a Zellij WASM plugin.
9const ZELLIJ_TILE_DEPENDENCY: &str = "zellij-tile";
10
11#[derive(Deserialize)]
12pub struct Metadata {
13    packages: Vec<Package>,
14}
15
16impl Metadata {
17    pub fn read(workspace_root: &Path) -> rootcause::Result<Self> {
18        let mut command = Command::new("cargo");
19        command
20            .args(["metadata", "--format-version=1", "--no-deps"])
21            .current_dir(workspace_root);
22        let output = command.output().context("failed to spawn cargo metadata")?;
23        output.status.exit_ok().context("cargo metadata failed")?;
24        Self::from_slice(&output.stdout)
25    }
26
27    pub fn native_audit_bin_paths(&self) -> Vec<PathBuf> {
28        let mut paths = self
29            .packages
30            .iter()
31            .filter(|package| !package.is_zellij_plugin())
32            .flat_map(|package| package.targets.iter())
33            .filter(|target| target.is_default_bin())
34            .map(|target| Path::new("./target/release").join(&target.name))
35            .collect::<Vec<_>>();
36        paths.sort();
37        paths
38    }
39
40    pub fn native_bin_package_names(&self) -> Vec<&str> {
41        let mut package_names = self
42            .packages
43            .iter()
44            .filter(|package| !package.is_zellij_plugin())
45            .filter(|package| package.targets.iter().any(Target::is_default_bin))
46            .map(|package| package.name.as_str())
47            .collect::<Vec<_>>();
48        package_names.sort_unstable();
49        package_names
50    }
51
52    pub fn zellij_plugin_manifests(&self) -> Vec<PathBuf> {
53        let mut manifests = self
54            .packages
55            .iter()
56            .filter(|package| package.is_zellij_plugin())
57            .map(|package| package.manifest_path.clone())
58            .collect::<Vec<_>>();
59        manifests.sort();
60        manifests
61    }
62
63    pub fn zellij_plugin_package_names(&self) -> Vec<&str> {
64        let mut package_names = self
65            .packages
66            .iter()
67            .filter(|package| package.is_zellij_plugin())
68            .map(|package| package.name.as_str())
69            .collect::<Vec<_>>();
70        package_names.sort_unstable();
71        package_names
72    }
73
74    fn from_slice(metadata: &[u8]) -> rootcause::Result<Self> {
75        Ok(serde_json::from_slice::<Self>(metadata).context("failed to parse cargo metadata")?)
76    }
77}
78
79#[derive(Deserialize)]
80struct Dependency {
81    #[serde(default)]
82    kind: Option<String>,
83    name: String,
84}
85
86#[derive(Deserialize)]
87struct Package {
88    dependencies: Vec<Dependency>,
89    manifest_path: PathBuf,
90    name: String,
91    targets: Vec<Target>,
92}
93
94impl Package {
95    fn is_zellij_plugin(&self) -> bool {
96        self.dependencies
97            .iter()
98            .any(|dependency| dependency.name == ZELLIJ_TILE_DEPENDENCY && dependency.kind.is_none())
99    }
100}
101
102#[derive(Deserialize)]
103struct Target {
104    kind: Vec<String>,
105    name: String,
106    #[serde(default, rename = "required-features")]
107    required_features: Vec<String>,
108}
109
110impl Target {
111    fn is_default_bin(&self) -> bool {
112        self.kind.iter().any(|kind| kind == "bin") && self.required_features.is_empty()
113    }
114}
115
116#[cfg(test)]
117mod tests {
118    use super::*;
119
120    #[test]
121    fn test_native_audit_bin_paths_uses_native_bin_targets() {
122        assert2::assert!(let Ok(metadata) = Metadata::from_slice(metadata_fixture()));
123
124        pretty_assertions::assert_eq!(
125            metadata.native_audit_bin_paths(),
126            vec![
127                PathBuf::from("./target/release/evoke"),
128                PathBuf::from("./target/release/fixture-tool"),
129                PathBuf::from("./target/release/zj"),
130            ]
131        );
132    }
133
134    #[test]
135    fn test_native_bin_package_names_uses_native_bin_targets() {
136        assert2::assert!(let Ok(metadata) = Metadata::from_slice(metadata_fixture()));
137
138        pretty_assertions::assert_eq!(metadata.native_bin_package_names(), vec!["evoke", "fixture-tool", "zj"]);
139    }
140
141    #[test]
142    fn test_zellij_plugin_manifests_uses_zellij_tile_dependency() {
143        assert2::assert!(let Ok(metadata) = Metadata::from_slice(metadata_fixture()));
144
145        pretty_assertions::assert_eq!(
146            metadata.zellij_plugin_manifests(),
147            vec![
148                PathBuf::from("/repo/moved/zcp/Cargo.toml"),
149                PathBuf::from("/repo/yog/zj/plugins/agg/Cargo.toml"),
150            ]
151        );
152    }
153
154    #[test]
155    fn test_zellij_plugin_package_names_uses_zellij_tile_dependency() {
156        assert2::assert!(let Ok(metadata) = Metadata::from_slice(metadata_fixture()));
157
158        pretty_assertions::assert_eq!(metadata.zellij_plugin_package_names(), vec!["agg", "zellij-copy"]);
159    }
160
161    fn metadata_fixture() -> &'static [u8] {
162        br#"
163            {
164              "packages": [
165                {
166                  "dependencies": [],
167                  "manifest_path": "/repo/yog/evoke/Cargo.toml",
168                  "name": "evoke",
169                  "targets": [
170                    { "kind": ["bin"], "name": "evoke" },
171                    { "kind": ["lib"], "name": "evoke" }
172                  ]
173                },
174                {
175                  "dependencies": [],
176                  "manifest_path": "/repo/yog/zj/cli/Cargo.toml",
177                  "name": "zj",
178                  "targets": [
179                    { "kind": ["bin"], "name": "zj" }
180                  ]
181                },
182                {
183                  "dependencies": [
184                    { "kind": null, "name": "zellij-tile" }
185                  ],
186                  "manifest_path": "/repo/yog/zj/plugins/agg/Cargo.toml",
187                  "name": "agg",
188                  "targets": [
189                    { "kind": ["bin"], "name": "agg" }
190                  ]
191                },
192                {
193                  "dependencies": [
194                    { "kind": null, "name": "zellij-tile" }
195                  ],
196                  "manifest_path": "/repo/moved/zcp/Cargo.toml",
197                  "name": "zellij-copy",
198                  "targets": [
199                    { "kind": ["bin"], "name": "zcp" }
200                  ]
201                },
202                {
203                  "dependencies": [],
204                  "manifest_path": "/repo/yog/feature-bin/Cargo.toml",
205                  "name": "feature-bin",
206                  "targets": [
207                    {
208                      "kind": ["bin"],
209                      "name": "feature-bin",
210                      "required-features": ["cli"]
211                    }
212                  ]
213                },
214                {
215                  "dependencies": [
216                    { "kind": "dev", "name": "zellij-tile" }
217                  ],
218                  "manifest_path": "/repo/yog/fixture-tool/Cargo.toml",
219                  "name": "fixture-tool",
220                  "targets": [
221                    { "kind": ["bin"], "name": "fixture-tool" }
222                  ]
223                }
224              ]
225            }
226        "#
227    }
228}