1use std::path::Path;
2use std::path::PathBuf;
3use std::process::Command;
4
5use rootcause::prelude::ResultExt;
6use serde::Deserialize;
7
8const 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}