1use crate::contracts;
7use origin_manifest::{Capability, Manifest};
8use std::fmt::Write as _;
9use std::path::{Path, PathBuf};
10
11const MARKER: &str = "Generated from app.toml";
14
15pub fn run(root: &Path) -> Result<(), String> {
17 if let Some(contracts) = contracts_target(root) {
21 write_if_changed(&contracts, &contracts::render()?)?;
22 println!("generated {}", relative(root, &contracts));
23 }
24
25 let manifests = find_manifests(root)?;
26 if manifests.is_empty() {
27 return Err(format!("no app.toml found under {}", root.display()));
28 }
29
30 for path in manifests {
31 let manifest = load(&path)?;
32 let directory = tauri_directory(&path);
33
34 for (file, contents) in outputs(&manifest, &directory)? {
35 write_if_changed(&file, &contents)?;
36 }
37
38 remove_stale_capabilities(&manifest, &directory)?;
39 println!("generated files for {}", relative(root, &path));
40 }
41
42 Ok(())
43}
44
45pub fn check(root: &Path) -> Result<(), String> {
50 let manifests = find_manifests(root)?;
51 if manifests.is_empty() {
52 return Err(format!("no app.toml found under {}", root.display()));
53 }
54
55 let mut problems = Vec::new();
56
57 check_contracts(root, &mut problems)?;
58
59 for path in manifests {
60 let manifest = load(&path)?;
61 let directory = tauri_directory(&path);
62
63 check_manifest_outputs(root, &manifest, &directory, &mut problems)?;
64 check_stale_capabilities(root, &manifest, &directory, &mut problems)?;
65 }
66
67 if problems.is_empty() {
68 println!("generated files: up to date");
69 return Ok(());
70 }
71
72 let mut report = String::from("generated files are out of date:\n");
73 for problem in &problems {
74 let _ = writeln!(report, " - {problem}");
75 }
76 Err(report)
77}
78
79fn check_contracts(root: &Path, problems: &mut Vec<String>) -> Result<(), String> {
82 let Some(contracts_path) = contracts_target(root) else {
83 return Ok(());
84 };
85
86 match std::fs::read_to_string(&contracts_path) {
87 Ok(actual) if actual == contracts::render()? => {}
88 Ok(_) => problems.push(format!(
89 "{} no longer matches the Rust contracts — run `cargo xtask generate`",
90 relative(root, &contracts_path)
91 )),
92 Err(_) => problems.push(format!(
93 "{} is missing — run `cargo xtask generate`",
94 relative(root, &contracts_path)
95 )),
96 }
97
98 Ok(())
99}
100
101fn check_manifest_outputs(
104 root: &Path,
105 manifest: &Manifest,
106 directory: &Path,
107 problems: &mut Vec<String>,
108) -> Result<(), String> {
109 for (file, expected) in outputs(manifest, directory)? {
110 match std::fs::read_to_string(&file) {
111 Ok(actual) if actual == expected => {}
112 Ok(_) => problems.push(format!(
113 "{} differs from what app.toml describes — run `cargo xtask generate`",
114 relative(root, &file)
115 )),
116 Err(_) => problems.push(format!(
117 "{} is missing — run `cargo xtask generate`",
118 relative(root, &file)
119 )),
120 }
121 }
122
123 Ok(())
124}
125
126fn check_stale_capabilities(
129 root: &Path,
130 manifest: &Manifest,
131 directory: &Path,
132 problems: &mut Vec<String>,
133) -> Result<(), String> {
134 for stale in stale_capabilities(manifest, directory)? {
135 problems.push(format!(
136 "{} is generated but no window uses its profile — run `cargo xtask generate`",
137 relative(root, &stale)
138 ));
139 }
140
141 Ok(())
142}
143
144fn contracts_target(root: &Path) -> Option<PathBuf> {
146 let path = contracts::output_path(root);
147 path.parent().is_some_and(Path::is_dir).then_some(path)
148}
149
150fn outputs(manifest: &Manifest, tauri_directory: &Path) -> Result<Vec<(PathBuf, String)>, String> {
152 let mut outputs = Vec::new();
153 let capabilities = tauri_directory.join("capabilities");
154
155 for capability in Capability::from_manifest(manifest) {
156 let json = serde_json::to_string_pretty(&capability)
157 .map_err(|error| format!("cannot encode capability: {error}"))?;
158
159 outputs.push((
160 capabilities.join(capability.file_name()),
161 format!("{json}\n"),
162 ));
163 }
164
165 Ok(outputs)
166}
167
168fn stale_capabilities(manifest: &Manifest, tauri_directory: &Path) -> Result<Vec<PathBuf>, String> {
170 let capabilities = tauri_directory.join("capabilities");
171 if !capabilities.exists() {
172 return Ok(Vec::new());
173 }
174
175 let current: Vec<String> = Capability::from_manifest(manifest)
176 .iter()
177 .map(Capability::file_name)
178 .collect();
179
180 let mut stale = Vec::new();
181 let entries = std::fs::read_dir(&capabilities)
182 .map_err(|error| format!("cannot read {}: {error}", capabilities.display()))?;
183
184 for entry in entries.filter_map(Result::ok) {
185 let path = entry.path();
186 let name = path
187 .file_name()
188 .unwrap_or_default()
189 .to_string_lossy()
190 .into_owned();
191
192 if path.extension().is_none_or(|extension| extension != "json") || current.contains(&name) {
193 continue;
194 }
195
196 let contents = std::fs::read_to_string(&path).unwrap_or_default();
199 if contents.contains(MARKER) {
200 stale.push(path);
201 }
202 }
203
204 Ok(stale)
205}
206
207fn remove_stale_capabilities(manifest: &Manifest, tauri_directory: &Path) -> Result<(), String> {
208 for path in stale_capabilities(manifest, tauri_directory)? {
209 std::fs::remove_file(&path)
210 .map_err(|error| format!("cannot remove {}: {error}", path.display()))?;
211 println!("removed stale {}", path.display());
212 }
213 Ok(())
214}
215
216fn write_if_changed(path: &Path, contents: &str) -> Result<(), String> {
219 if std::fs::read_to_string(path).is_ok_and(|existing| existing == contents) {
220 return Ok(());
221 }
222
223 if let Some(parent) = path.parent() {
224 std::fs::create_dir_all(parent)
225 .map_err(|error| format!("cannot create {}: {error}", parent.display()))?;
226 }
227
228 std::fs::write(path, contents)
229 .map_err(|error| format!("cannot write {}: {error}", path.display()))
230}
231
232fn load(path: &Path) -> Result<Manifest, String> {
233 Manifest::load(path).map_err(|error| error.to_string())
234}
235
236fn tauri_directory(manifest: &Path) -> PathBuf {
238 manifest
239 .parent()
240 .unwrap_or(Path::new("."))
241 .join("src-tauri")
242}
243
244pub(crate) fn find_manifests(root: &Path) -> Result<Vec<PathBuf>, String> {
248 let mut manifests = Vec::new();
249 collect_manifests(root, &mut manifests)?;
250 manifests.sort();
251 Ok(manifests)
252}
253
254fn collect_manifests(directory: &Path, found: &mut Vec<PathBuf>) -> Result<(), String> {
255 let manifest = directory.join("app.toml");
256 if manifest.is_file() {
257 found.push(manifest);
258 }
259
260 let entries = std::fs::read_dir(directory)
261 .map_err(|error| format!("cannot read {}: {error}", directory.display()))?;
262
263 for entry in entries.filter_map(Result::ok) {
264 let path = entry.path();
265 if !path.is_dir() {
266 continue;
267 }
268
269 let name = path
270 .file_name()
271 .unwrap_or_default()
272 .to_string_lossy()
273 .into_owned();
274 if matches!(
277 name.as_str(),
278 "target" | "node_modules" | "dist" | "gen" | ".git" | "plan" | "fixtures" | "templates"
279 ) {
280 continue;
281 }
282
283 collect_manifests(&path, found)?;
284 }
285
286 Ok(())
287}
288
289fn relative(root: &Path, path: &Path) -> String {
290 path.strip_prefix(root)
291 .unwrap_or(path)
292 .to_string_lossy()
293 .into_owned()
294}