use crate::contracts;
use origin_manifest::{Capability, Manifest};
use std::fmt::Write as _;
use std::path::{Path, PathBuf};
const MARKER: &str = "Generated from app.toml";
pub fn run(root: &Path) -> Result<(), String> {
if let Some(contracts) = contracts_target(root) {
write_if_changed(&contracts, &contracts::render()?)?;
println!("generated {}", relative(root, &contracts));
}
let manifests = find_manifests(root)?;
if manifests.is_empty() {
return Err(format!("no app.toml found under {}", root.display()));
}
for path in manifests {
let manifest = load(&path)?;
let directory = tauri_directory(&path);
for (file, contents) in outputs(&manifest, &directory)? {
write_if_changed(&file, &contents)?;
}
remove_stale_capabilities(&manifest, &directory)?;
println!("generated files for {}", relative(root, &path));
}
Ok(())
}
pub fn check(root: &Path) -> Result<(), String> {
let manifests = find_manifests(root)?;
if manifests.is_empty() {
return Err(format!("no app.toml found under {}", root.display()));
}
let mut problems = Vec::new();
check_contracts(root, &mut problems)?;
for path in manifests {
let manifest = load(&path)?;
let directory = tauri_directory(&path);
check_manifest_outputs(root, &manifest, &directory, &mut problems)?;
check_stale_capabilities(root, &manifest, &directory, &mut problems)?;
}
if problems.is_empty() {
println!("generated files: up to date");
return Ok(());
}
let mut report = String::from("generated files are out of date:\n");
for problem in &problems {
let _ = writeln!(report, " - {problem}");
}
Err(report)
}
fn check_contracts(root: &Path, problems: &mut Vec<String>) -> Result<(), String> {
let Some(contracts_path) = contracts_target(root) else {
return Ok(());
};
match std::fs::read_to_string(&contracts_path) {
Ok(actual) if actual == contracts::render()? => {}
Ok(_) => problems.push(format!(
"{} no longer matches the Rust contracts — run `cargo xtask generate`",
relative(root, &contracts_path)
)),
Err(_) => problems.push(format!(
"{} is missing — run `cargo xtask generate`",
relative(root, &contracts_path)
)),
}
Ok(())
}
fn check_manifest_outputs(
root: &Path,
manifest: &Manifest,
directory: &Path,
problems: &mut Vec<String>,
) -> Result<(), String> {
for (file, expected) in outputs(manifest, directory)? {
match std::fs::read_to_string(&file) {
Ok(actual) if actual == expected => {}
Ok(_) => problems.push(format!(
"{} differs from what app.toml describes — run `cargo xtask generate`",
relative(root, &file)
)),
Err(_) => problems.push(format!(
"{} is missing — run `cargo xtask generate`",
relative(root, &file)
)),
}
}
Ok(())
}
fn check_stale_capabilities(
root: &Path,
manifest: &Manifest,
directory: &Path,
problems: &mut Vec<String>,
) -> Result<(), String> {
for stale in stale_capabilities(manifest, directory)? {
problems.push(format!(
"{} is generated but no window uses its profile — run `cargo xtask generate`",
relative(root, &stale)
));
}
Ok(())
}
fn contracts_target(root: &Path) -> Option<PathBuf> {
let path = contracts::output_path(root);
path.parent().is_some_and(Path::is_dir).then_some(path)
}
fn outputs(manifest: &Manifest, tauri_directory: &Path) -> Result<Vec<(PathBuf, String)>, String> {
let mut outputs = Vec::new();
let capabilities = tauri_directory.join("capabilities");
for capability in Capability::from_manifest(manifest) {
let json = serde_json::to_string_pretty(&capability)
.map_err(|error| format!("cannot encode capability: {error}"))?;
outputs.push((
capabilities.join(capability.file_name()),
format!("{json}\n"),
));
}
Ok(outputs)
}
fn stale_capabilities(manifest: &Manifest, tauri_directory: &Path) -> Result<Vec<PathBuf>, String> {
let capabilities = tauri_directory.join("capabilities");
if !capabilities.exists() {
return Ok(Vec::new());
}
let current: Vec<String> = Capability::from_manifest(manifest)
.iter()
.map(Capability::file_name)
.collect();
let mut stale = Vec::new();
let entries = std::fs::read_dir(&capabilities)
.map_err(|error| format!("cannot read {}: {error}", capabilities.display()))?;
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
if path.extension().is_none_or(|extension| extension != "json") || current.contains(&name) {
continue;
}
let contents = std::fs::read_to_string(&path).unwrap_or_default();
if contents.contains(MARKER) {
stale.push(path);
}
}
Ok(stale)
}
fn remove_stale_capabilities(manifest: &Manifest, tauri_directory: &Path) -> Result<(), String> {
for path in stale_capabilities(manifest, tauri_directory)? {
std::fs::remove_file(&path)
.map_err(|error| format!("cannot remove {}: {error}", path.display()))?;
println!("removed stale {}", path.display());
}
Ok(())
}
fn write_if_changed(path: &Path, contents: &str) -> Result<(), String> {
if std::fs::read_to_string(path).is_ok_and(|existing| existing == contents) {
return Ok(());
}
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.map_err(|error| format!("cannot create {}: {error}", parent.display()))?;
}
std::fs::write(path, contents)
.map_err(|error| format!("cannot write {}: {error}", path.display()))
}
fn load(path: &Path) -> Result<Manifest, String> {
Manifest::load(path).map_err(|error| error.to_string())
}
fn tauri_directory(manifest: &Path) -> PathBuf {
manifest
.parent()
.unwrap_or(Path::new("."))
.join("src-tauri")
}
pub(crate) fn find_manifests(root: &Path) -> Result<Vec<PathBuf>, String> {
let mut manifests = Vec::new();
collect_manifests(root, &mut manifests)?;
manifests.sort();
Ok(manifests)
}
fn collect_manifests(directory: &Path, found: &mut Vec<PathBuf>) -> Result<(), String> {
let manifest = directory.join("app.toml");
if manifest.is_file() {
found.push(manifest);
}
let entries = std::fs::read_dir(directory)
.map_err(|error| format!("cannot read {}: {error}", directory.display()))?;
for entry in entries.filter_map(Result::ok) {
let path = entry.path();
if !path.is_dir() {
continue;
}
let name = path
.file_name()
.unwrap_or_default()
.to_string_lossy()
.into_owned();
if matches!(
name.as_str(),
"target" | "node_modules" | "dist" | "gen" | ".git" | "plan" | "fixtures" | "templates"
) {
continue;
}
collect_manifests(&path, found)?;
}
Ok(())
}
fn relative(root: &Path, path: &Path) -> String {
path.strip_prefix(root)
.unwrap_or(path)
.to_string_lossy()
.into_owned()
}